A production-ready GraphQL API server for managing Flash Invaders game data and Farcaster social integration. This service provides comprehensive endpoints for user management, flash data retrieval, leaderboards, progress tracking, and signup workflows with dual image storage support.
- GraphQL API with Apollo Server v3 and comprehensive schema
- Farcaster Integration via Neynar SDK for social authentication
- Database Management with Prisma ORM and PostgreSQL
- Dual Image Storage - Traditional S3 URLs and decentralized IPFS support
- User Management - Complete signup workflows and profile management
- Performance Optimization - Memory caching, database indexing, and query optimization
- Real-time Data - Leaderboards, trending cities, and progress tracking
- TypeScript - Fully typed codebase with strict type checking
- Production Ready - Error handling, authentication, and monitoring
- Runtime: Node.js >=19.9.0
- Language: TypeScript with strict mode
- GraphQL: Apollo Server v3 with introspection
- Database: PostgreSQL with Prisma ORM and optimized indexing
- Caching: In-memory caching with TTL strategies
- External APIs:
- Neynar SDK (Farcaster social data)
- Flash Invaders API (game data)
- IPFS via Pinata (decentralized storage)
- Package Manager: Yarn v1.22.22
- Security: API key authentication and encrypted signer storage
- Node.js >=19.9.0
- PostgreSQL database
- Yarn package manager
- Clone the repository:
git clone <repository-url>
cd flashcastr.api- Install dependencies:
yarn install-
Set up environment variables (see Environment Variables)
-
Generate Prisma client:
yarn prisma generate- Run database migrations (if applicable):
yarn prisma migrate devCreate a .env file in the root directory with the following variables:
# Database Configuration
DATABASE_URL="postgresql://username:password@localhost:5432/flashcastr"
# External API Keys
NEYNAR_API_KEY="your_neynar_api_key_here"
INVADERS_API_URL="https://api.space-invaders.com"
# Authentication & Security
API_KEY="your_secure_api_key_32_chars_min"
SIGNER_ENCRYPTION_KEY="your_32_byte_hex_encryption_key"
# Farcaster Configuration
FARCASTER_DEVELOPER_MNEMONIC="your twelve word mnemonic phrase for farcaster signer creation"
# Server Configuration
NODE_ENV="development"
PORT=4000Start the development server:
yarn devThe GraphQL server will be available at http://localhost:4000
Build the application:
yarn buildStart the production server:
yarn starttype User {
fid: Int!
username: String
auto_cast: Boolean
}type Flash {
flash_id: ID!
city: String
player: String
img: String # S3 image URL/key
ipfs_cid: String # IPFS content identifier
text: String
timestamp: String
flash_count: String
}type FlashcastrFlash {
id: Int!
flash_id: String!
user_fid: Int!
user_username: String
user_pfp_url: String
cast_hash: String
flash: Flash!
}users(username: String, fid: Int): [User!]!- Get users by username or FIDflashes(page: Int, limit: Int, fid: Int, username: String, city: String): [FlashcastrFlash!]!- Get paginated flashes with filteringglobalFlashes(page: Int, limit: Int, city: String, player: String): [Flash!]!- Get global flash data with city/player filtersflash(id: Int!): FlashcastrFlash- Get single flash by IDglobalFlash(flash_id: String!): Flash- Get global flash by flash_id
flashesSummary(fid: Int!): FlashesSummary!- Get user flash statistics and citiesgetLeaderboard(limit: Int = 100): [LeaderboardEntry!]!- Get user rankings (cached 1h)getTrendingCities(excludeParis: Boolean = true, hours: Int = 6): [TrendingCity!]!- Get trending cities (cached 24h)progress(fid: Int!, days: Int!, order: String = "ASC"): [DailyProgress!]!- Get daily activity heatmap datagetAllCities: [String!]!- Get complete list of citiesallFlashesPlayers(username: String): [String!]!- Get player names for autocomplete
pollSignupStatus(signer_uuid: String!, username: String!): PollSignupStatusResponse!- Poll signup status
setUserAutoCast(fid: Int!, auto_cast: Boolean!): User!- Update user auto-cast preference (requires API key)deleteUser(fid: Int!): DeleteUserResponse!- Delete user account and associated data (requires API key)initiateSignup(username: String!): InitiateSignupResponse!- Start Farcaster signup processsignup(fid: Int!, signer_uuid: String!, username: String!): SignupResponse!- Complete signup (deprecated - use initiateSignup + pollSignupStatus)
The API supports dual image storage:
- Traditional S3: Using the
imgfield (legacy) - IPFS: Using the
ipfs_cidfield (new)
- Gateway:
https://fuchsia-rich-lungfish-648.mypinata.cloud(configurable via IPFS_GATEWAY env) - URL Construction:
https://fuchsia-rich-lungfish-648.mypinata.cloud/ipfs/{cid} - Client Decision: Frontend applications can choose which image source to use
- Backup Strategy: S3 URLs provide fallback when IPFS is unavailable
The API returns both fields without transformation, allowing clients to:
- Use S3 URLs for immediate compatibility
- Use IPFS for decentralized content delivery
- Implement fallback strategies
flashcastr_users- User accounts and settingsflashcastr_flashes- User-flash relationships with cast dataflashes- Flash Invaders game data with image references
- Users can have multiple flashes
- Each flash references the original Flash Invaders data
- Flash data includes both S3 and IPFS image references
Most query operations are public and don't require authentication:
- All flash data queries (
flashes,globalFlashes, etc.) - Analytics queries (
getLeaderboard,getTrendingCities,progress) - User lookup (
users) - Signup initiation (
initiateSignup,pollSignupStatus)
User management mutations require API key authentication:
setUserAutoCast- Update user preferencesdeleteUser- Account deletion
Authentication Method:
// Include API key in request headers
headers: {
'X-API-KEY': 'your_api_key_here'
}Error Response:
{
"errors": [{
"message": "Unauthorized",
"extensions": { "code": "UNAUTHENTICATED" }
}]
}- Trending Cities: 24-hour TTL for trending city calculations
- Leaderboard: 1-hour TTL for user rankings and statistics
- Database Indexes: Optimized indexes for common query patterns
- Connection Pooling: Efficient PostgreSQL connection management
- Query Optimization: Direct SQL for complex aggregations
- Indexed Queries: Strategic indexing for timestamp and user-based queries
- Average Response Time: <100ms for cached queries, <500ms for database queries
- Concurrent Requests: Supports high concurrent load with connection pooling
- Error Handling: Comprehensive error responses with specific error codes
src/
├── index.ts # Main GraphQL server and resolvers
├── utils/
│ ├── api.invaders.fun/ # External API integration
│ │ ├── base.ts # Base API client
│ │ └── flashes.ts # Flash Invaders API
│ ├── auth.ts # Authentication utilities
│ ├── database/ # Database layer
│ │ ├── flashcastr/ # Flashcastr-specific queries
│ │ ├── flashes/ # Flash data queries
│ │ ├── users/ # User management queries
│ │ ├── postgres.ts # Base PostgreSQL class
│ │ └── postgresClient.ts # Database connection
│ ├── encrypt/ # Encryption utilities
│ ├── ipfs.ts # IPFS URL utilities
│ ├── neynar/ # Farcaster/Neynar integration
│ └── tasks/ # Background task handlers
│ └── signup.ts # User signup workflows
import { useQuery } from '@apollo/client';
import { gql } from '@apollo/client';
const GET_LEADERBOARD = gql`
query GetLeaderboard($limit: Int) {
getLeaderboard(limit: $limit) {
username
pfp_url
flash_count
city_count
}
}
`;
function Leaderboard() {
const { data, loading, error } = useQuery(GET_LEADERBOARD, {
variables: { limit: 50 }
});
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
{data.getLeaderboard.map((entry, index) => (
<div key={entry.username}>
#{index + 1} {entry.username} - {entry.flash_count} flashes
</div>
))}
</div>
);
}const response = await fetch('http://localhost:4000/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `
query GetFlashes($page: Int, $limit: Int) {
flashes(page: $page, limit: $limit) {
flash_id
user_username
flash {
city
player
timestamp
}
}
}
`,
variables: { page: 1, limit: 20 }
})
});
const { data } = await response.json();The API exposes Prometheus metrics on port 9092:
| Endpoint | Description |
|---|---|
GET /metrics |
Prometheus metrics |
GET /health |
Health check |
Key metrics:
flashcastr_api_graphql_requests_total{operation_type, operation_name}- Request countflashcastr_api_graphql_errors_total{operation_type, operation_name}- Error countflashcastr_api_graphql_duration_seconds- Request duration histogramflashcastr_api_signups_initiated_total- Signup initiationsflashcastr_api_signups_completed_total- Completed signupsflashcastr_api_users_deleted_total- User deletionsflashcastr_api_cache_hits_total{cache_name}- Cache hitsflashcastr_api_cache_misses_total{cache_name}- Cache missesflashcastr_api_neynar_requests_total{endpoint, status}- Neynar API callsflashcastr_api_active_users_total- Active user count (gauge)flashcastr_api_total_flashes- Total flashes (gauge)flashcastr_api_uptime_seconds- Service uptimeflashcastr_api_memory_bytes{type}- Memory usage
The API sends distributed traces to Tempo via OpenTelemetry Protocol (OTLP).
Environment variable:
TEMPO_HTTP_ENDPOINT=http://tempo.railway.internal:4318/v1/tracesAuto-instrumented:
- HTTP requests
- GraphQL operations
- PostgreSQL queries
When TEMPO_HTTP_ENDPOINT is not set, tracing is disabled.
- Set up PostgreSQL database
- Configure environment variables
- Run database migrations:
yarn prisma migrate deploy - Generate Prisma client:
yarn prisma generate - Build application:
yarn build - Start server:
yarn start
- Configure proper
DATABASE_URLfor production database - Set up proper CORS policies for your frontend domain
- Use environment-specific API keys
- Enable database connection pooling
- Set up monitoring and logging
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes
- Run tests:
yarn build(ensure no TypeScript errors) - Commit changes (
git commit -m 'Add amazing feature') - Push to branch (
git push origin feature/amazing-feature) - Open a Pull Request
For questions, bug reports, or feature requests:
- Create an issue on GitHub
- Check existing documentation in
/docs - Review the GraphQL schema at
http://localhost:4000/graphql
ISC License - see LICENSE file for details