indie-api-backend/SKILL.md
Build production-ready Node.js backends for indie iOS apps. Covers REST/GraphQL APIs, authentication, database design, file uploads, rate limiting, and deployment.
npx skillsauth add abanoub-ashraf/manus-skills-import indie-api-backendInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
4 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Build production-ready Node.js backends for indie iOS apps. Covers REST/GraphQL APIs, authentication, database design, file uploads, rate limiting, and deployment.
Runtime: Node.js 20+ or Bun
Framework: Hono (lightweight) or Express
Database: PostgreSQL (Supabase/Neon)
ORM: Prisma
Validation: Zod
Auth: JWT + refresh tokens
File Storage: Cloudflare R2 or AWS S3
Deployment: Railway, Render, or Fly.io
Why Hono?
src/
├── index.ts # Entry point
├── routes/
│ ├── index.ts # Route registration
│ ├── auth.ts # Auth routes
│ ├── users.ts # User routes
│ └── sync.ts # Data sync routes
├── middleware/
│ ├── auth.ts # JWT verification
│ ├── rateLimit.ts # Rate limiting
│ └── validate.ts # Request validation
├── services/
│ ├── auth.ts # Auth business logic
│ ├── user.ts # User business logic
│ └── subscription.ts # Subscription logic
├── lib/
│ ├── db.ts # Prisma client
│ ├── jwt.ts # JWT helpers
│ ├── hash.ts # Password hashing
│ └── storage.ts # File storage
├── schemas/
│ └── index.ts # Zod schemas
└── types/
└── index.ts # TypeScript types
prisma/
└── schema.prisma # Database schema
npm init -y
npm install hono @hono/node-server
npm install prisma @prisma/client zod bcryptjs jsonwebtoken
npm install -D typescript @types/node @types/bcryptjs @types/jsonwebtoken tsx
// src/index.ts
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { secureHeaders } from 'hono/secure-headers'
import { authRoutes } from './routes/auth'
import { userRoutes } from './routes/users'
import { syncRoutes } from './routes/sync'
import { errorHandler } from './middleware/error'
const app = new Hono()
// Global middleware
app.use('*', logger())
app.use('*', secureHeaders())
app.use('*', cors({
origin: ['https://yourapp.com'],
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
allowHeaders: ['Content-Type', 'Authorization'],
credentials: true,
}))
// Health check
app.get('/health', (c) => c.json({ status: 'ok' }))
// Routes
app.route('/auth', authRoutes)
app.route('/users', userRoutes)
app.route('/sync', syncRoutes)
// Error handling
app.onError(errorHandler)
// 404 handler
app.notFound((c) => c.json({ error: 'Not found' }, 404))
const port = parseInt(process.env.PORT || '3000')
console.log(`Server running on port ${port}`)
serve({
fetch: app.fetch,
port,
})
export default app
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*"]
}
// package.json
{
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"db:push": "prisma db push",
"db:generate": "prisma generate",
"db:studio": "prisma studio"
}
}
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
hashedPassword String?
name String?
avatarUrl String?
// OAuth
appleId String? @unique
googleId String? @unique
// Status
isVerified Boolean @default(false)
isActive Boolean @default(true)
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
lastLoginAt DateTime?
// Relations
refreshTokens RefreshToken[]
subscription Subscription?
devices Device[]
data UserData[]
}
model RefreshToken {
id String @id @default(cuid())
token String @unique
userId String
expiresAt DateTime
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
}
model Device {
id String @id @default(cuid())
userId String
deviceToken String? // For push notifications
platform String // ios, android
appVersion String?
osVersion String?
lastActiveAt DateTime @default(now())
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([userId, deviceToken])
@@index([userId])
}
model Subscription {
id String @id @default(cuid())
userId String @unique
// App Store data
originalTransactionId String @unique
productId String
environment String // sandbox, production
// Status
status String // active, expired, canceled, grace_period
expiresAt DateTime?
gracePeriodExpiresAt DateTime?
// Timestamps
purchasedAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model UserData {
id String @id @default(cuid())
userId String
key String
value Json
version Int @default(1)
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([userId, key])
@@index([userId])
}
// src/lib/db.ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
})
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma
}
// src/lib/jwt.ts
import jwt from 'jsonwebtoken'
const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET!
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET!
interface TokenPayload {
userId: string
email: string
}
export function generateAccessToken(payload: TokenPayload): string {
return jwt.sign(payload, ACCESS_SECRET, { expiresIn: '15m' })
}
export function generateRefreshToken(payload: TokenPayload): string {
return jwt.sign(payload, REFRESH_SECRET, { expiresIn: '30d' })
}
export function verifyAccessToken(token: string): TokenPayload {
return jwt.verify(token, ACCESS_SECRET) as TokenPayload
}
export function verifyRefreshToken(token: string): TokenPayload {
return jwt.verify(token, REFRESH_SECRET) as TokenPayload
}
// src/lib/hash.ts
import bcrypt from 'bcryptjs'
const SALT_ROUNDS = 12
export async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS)
}
export async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash)
}
// src/middleware/auth.ts
import { Context, Next } from 'hono'
import { verifyAccessToken } from '../lib/jwt'
export async function authMiddleware(c: Context, next: Next) {
const authHeader = c.req.header('Authorization')
if (!authHeader?.startsWith('Bearer ')) {
return c.json({ error: 'Missing authorization header' }, 401)
}
const token = authHeader.slice(7)
try {
const payload = verifyAccessToken(token)
c.set('userId', payload.userId)
c.set('email', payload.email)
await next()
} catch (error) {
return c.json({ error: 'Invalid or expired token' }, 401)
}
}
// src/routes/auth.ts
import { Hono } from 'hono'
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'
import { prisma } from '../lib/db'
import { hashPassword, verifyPassword } from '../lib/hash'
import { generateAccessToken, generateRefreshToken, verifyRefreshToken } from '../lib/jwt'
import crypto from 'crypto'
export const authRoutes = new Hono()
// Schemas
const registerSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
name: z.string().optional(),
})
const loginSchema = z.object({
email: z.string().email(),
password: z.string(),
})
const refreshSchema = z.object({
refreshToken: z.string(),
})
// Register
authRoutes.post('/register', zValidator('json', registerSchema), async (c) => {
const { email, password, name } = c.req.valid('json')
// Check if user exists
const existing = await prisma.user.findUnique({ where: { email } })
if (existing) {
return c.json({ error: 'Email already registered' }, 400)
}
// Create user
const hashedPassword = await hashPassword(password)
const user = await prisma.user.create({
data: {
email,
hashedPassword,
name,
},
})
// Generate tokens
const accessToken = generateAccessToken({ userId: user.id, email: user.email })
const refreshToken = generateRefreshToken({ userId: user.id, email: user.email })
// Store refresh token
await prisma.refreshToken.create({
data: {
token: crypto.createHash('sha256').update(refreshToken).digest('hex'),
userId: user.id,
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days
},
})
return c.json({
user: {
id: user.id,
email: user.email,
name: user.name,
},
accessToken,
refreshToken,
}, 201)
})
// Login
authRoutes.post('/login', zValidator('json', loginSchema), async (c) => {
const { email, password } = c.req.valid('json')
// Find user
const user = await prisma.user.findUnique({ where: { email } })
if (!user || !user.hashedPassword) {
return c.json({ error: 'Invalid credentials' }, 401)
}
// Verify password
const valid = await verifyPassword(password, user.hashedPassword)
if (!valid) {
return c.json({ error: 'Invalid credentials' }, 401)
}
// Update last login
await prisma.user.update({
where: { id: user.id },
data: { lastLoginAt: new Date() },
})
// Generate tokens
const accessToken = generateAccessToken({ userId: user.id, email: user.email })
const refreshToken = generateRefreshToken({ userId: user.id, email: user.email })
// Store refresh token
await prisma.refreshToken.create({
data: {
token: crypto.createHash('sha256').update(refreshToken).digest('hex'),
userId: user.id,
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
},
})
return c.json({
user: {
id: user.id,
email: user.email,
name: user.name,
},
accessToken,
refreshToken,
})
})
// Refresh token
authRoutes.post('/refresh', zValidator('json', refreshSchema), async (c) => {
const { refreshToken } = c.req.valid('json')
try {
// Verify token
const payload = verifyRefreshToken(refreshToken)
// Check if token exists in DB
const tokenHash = crypto.createHash('sha256').update(refreshToken).digest('hex')
const storedToken = await prisma.refreshToken.findUnique({
where: { token: tokenHash },
})
if (!storedToken || storedToken.expiresAt < new Date()) {
return c.json({ error: 'Invalid refresh token' }, 401)
}
// Delete old token
await prisma.refreshToken.delete({ where: { id: storedToken.id } })
// Generate new tokens
const newAccessToken = generateAccessToken({ userId: payload.userId, email: payload.email })
const newRefreshToken = generateRefreshToken({ userId: payload.userId, email: payload.email })
// Store new refresh token
await prisma.refreshToken.create({
data: {
token: crypto.createHash('sha256').update(newRefreshToken).digest('hex'),
userId: payload.userId,
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
},
})
return c.json({
accessToken: newAccessToken,
refreshToken: newRefreshToken,
})
} catch (error) {
return c.json({ error: 'Invalid refresh token' }, 401)
}
})
// Logout
authRoutes.post('/logout', zValidator('json', refreshSchema), async (c) => {
const { refreshToken } = c.req.valid('json')
const tokenHash = crypto.createHash('sha256').update(refreshToken).digest('hex')
await prisma.refreshToken.deleteMany({ where: { token: tokenHash } })
return c.json({ success: true })
})
// src/routes/auth.ts (add to existing file)
import { verifyAppleToken } from '../lib/apple'
const appleLoginSchema = z.object({
identityToken: z.string(),
authorizationCode: z.string(),
name: z.string().optional(),
})
authRoutes.post('/apple', zValidator('json', appleLoginSchema), async (c) => {
const { identityToken, name } = c.req.valid('json')
try {
// Verify Apple identity token
const appleUser = await verifyAppleToken(identityToken)
// Find or create user
let user = await prisma.user.findUnique({
where: { appleId: appleUser.sub },
})
if (!user) {
// Check if email exists (link accounts)
if (appleUser.email) {
user = await prisma.user.findUnique({
where: { email: appleUser.email },
})
if (user) {
// Link Apple ID to existing account
user = await prisma.user.update({
where: { id: user.id },
data: { appleId: appleUser.sub },
})
}
}
// Create new user
if (!user) {
user = await prisma.user.create({
data: {
appleId: appleUser.sub,
email: appleUser.email || `${appleUser.sub}@privaterelay.appleid.com`,
name: name || undefined,
isVerified: true,
},
})
}
}
// Generate tokens
const accessToken = generateAccessToken({ userId: user.id, email: user.email })
const refreshToken = generateRefreshToken({ userId: user.id, email: user.email })
// Store refresh token
await prisma.refreshToken.create({
data: {
token: crypto.createHash('sha256').update(refreshToken).digest('hex'),
userId: user.id,
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
},
})
return c.json({
user: {
id: user.id,
email: user.email,
name: user.name,
},
accessToken,
refreshToken,
})
} catch (error) {
return c.json({ error: 'Apple authentication failed' }, 401)
}
})
// src/lib/apple.ts
import jwt from 'jsonwebtoken'
import jwksClient from 'jwks-rsa'
const client = jwksClient({
jwksUri: 'https://appleid.apple.com/auth/keys',
cache: true,
rateLimit: true,
})
interface AppleTokenPayload {
iss: string
sub: string // User ID
aud: string // Your app bundle ID
email?: string
email_verified?: boolean
}
export async function verifyAppleToken(identityToken: string): Promise<AppleTokenPayload> {
const decoded = jwt.decode(identityToken, { complete: true })
if (!decoded || !decoded.header.kid) {
throw new Error('Invalid token')
}
const key = await client.getSigningKey(decoded.header.kid)
const publicKey = key.getPublicKey()
const payload = jwt.verify(identityToken, publicKey, {
algorithms: ['RS256'],
issuer: 'https://appleid.apple.com',
audience: process.env.APPLE_BUNDLE_ID,
}) as AppleTokenPayload
return payload
}
// src/routes/sync.ts
import { Hono } from 'hono'
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'
import { prisma } from '../lib/db'
import { authMiddleware } from '../middleware/auth'
export const syncRoutes = new Hono()
// All sync routes require auth
syncRoutes.use('*', authMiddleware)
// Schemas
const syncPushSchema = z.object({
changes: z.array(z.object({
key: z.string(),
value: z.any(),
version: z.number(),
deletedAt: z.string().datetime().optional(),
})),
lastSyncedAt: z.string().datetime().optional(),
})
const syncPullSchema = z.object({
lastSyncedAt: z.string().datetime().optional(),
keys: z.array(z.string()).optional(),
})
// Push changes from client to server
syncRoutes.post('/push', zValidator('json', syncPushSchema), async (c) => {
const userId = c.get('userId')
const { changes, lastSyncedAt } = c.req.valid('json')
const results = []
for (const change of changes) {
try {
// Check for conflicts
const existing = await prisma.userData.findUnique({
where: { userId_key: { userId, key: change.key } },
})
if (existing && existing.version >= change.version) {
// Conflict - server wins
results.push({
key: change.key,
status: 'conflict',
serverVersion: existing.version,
serverValue: existing.value,
})
continue
}
// Upsert the data
if (change.deletedAt) {
// Soft delete
await prisma.userData.delete({
where: { userId_key: { userId, key: change.key } },
})
results.push({ key: change.key, status: 'deleted' })
} else {
await prisma.userData.upsert({
where: { userId_key: { userId, key: change.key } },
create: {
userId,
key: change.key,
value: change.value,
version: change.version,
},
update: {
value: change.value,
version: change.version,
},
})
results.push({ key: change.key, status: 'synced' })
}
} catch (error) {
results.push({ key: change.key, status: 'error' })
}
}
return c.json({
results,
syncedAt: new Date().toISOString(),
})
})
// Pull changes from server to client
syncRoutes.post('/pull', zValidator('json', syncPullSchema), async (c) => {
const userId = c.get('userId')
const { lastSyncedAt, keys } = c.req.valid('json')
const where: any = { userId }
if (lastSyncedAt) {
where.updatedAt = { gt: new Date(lastSyncedAt) }
}
if (keys && keys.length > 0) {
where.key = { in: keys }
}
const data = await prisma.userData.findMany({
where,
select: {
key: true,
value: true,
version: true,
updatedAt: true,
},
})
return c.json({
data,
syncedAt: new Date().toISOString(),
})
})
// Get single item
syncRoutes.get('/data/:key', async (c) => {
const userId = c.get('userId')
const key = c.req.param('key')
const data = await prisma.userData.findUnique({
where: { userId_key: { userId, key } },
})
if (!data) {
return c.json({ error: 'Not found' }, 404)
}
return c.json(data)
})
// src/middleware/rateLimit.ts
import { Context, Next } from 'hono'
interface RateLimitStore {
[key: string]: {
count: number
resetAt: number
}
}
const store: RateLimitStore = {}
interface RateLimitOptions {
windowMs: number
max: number
keyGenerator?: (c: Context) => string
}
export function rateLimit(options: RateLimitOptions) {
const { windowMs, max, keyGenerator } = options
return async (c: Context, next: Next) => {
const key = keyGenerator
? keyGenerator(c)
: c.req.header('x-forwarded-for') || 'unknown'
const now = Date.now()
if (!store[key] || store[key].resetAt < now) {
store[key] = {
count: 1,
resetAt: now + windowMs,
}
} else {
store[key].count++
}
// Set headers
c.header('X-RateLimit-Limit', max.toString())
c.header('X-RateLimit-Remaining', Math.max(0, max - store[key].count).toString())
c.header('X-RateLimit-Reset', store[key].resetAt.toString())
if (store[key].count > max) {
return c.json({ error: 'Too many requests' }, 429)
}
await next()
}
}
// src/index.ts
import { rateLimit } from './middleware/rateLimit'
// Apply to auth routes
app.use('/auth/*', rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
}))
// Stricter limit for login
app.use('/auth/login', rateLimit({
windowMs: 15 * 60 * 1000,
max: 10,
}))
// API routes
app.use('/api/*', rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100,
keyGenerator: (c) => c.get('userId') || c.req.header('x-forwarded-for') || 'unknown',
}))
// src/schemas/index.ts
import { z } from 'zod'
// Common schemas
export const paginationSchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().positive().max(100).default(20),
})
export const idParamSchema = z.object({
id: z.string().cuid(),
})
// User schemas
export const updateUserSchema = z.object({
name: z.string().min(1).max(100).optional(),
avatarUrl: z.string().url().optional(),
})
// Device schemas
export const registerDeviceSchema = z.object({
deviceToken: z.string(),
platform: z.enum(['ios', 'android']),
appVersion: z.string().optional(),
osVersion: z.string().optional(),
})
// src/middleware/validate.ts
import { Context, Next } from 'hono'
import { z } from 'zod'
export function validateBody<T extends z.ZodSchema>(schema: T) {
return async (c: Context, next: Next) => {
try {
const body = await c.req.json()
const validated = schema.parse(body)
c.set('validatedBody', validated)
await next()
} catch (error) {
if (error instanceof z.ZodError) {
return c.json({
error: 'Validation failed',
details: error.errors,
}, 400)
}
throw error
}
}
}
export function validateQuery<T extends z.ZodSchema>(schema: T) {
return async (c: Context, next: Next) => {
try {
const query = c.req.query()
const validated = schema.parse(query)
c.set('validatedQuery', validated)
await next()
} catch (error) {
if (error instanceof z.ZodError) {
return c.json({
error: 'Validation failed',
details: error.errors,
}, 400)
}
throw error
}
}
}
// src/middleware/error.ts
import { Context } from 'hono'
import { HTTPException } from 'hono/http-exception'
export class AppError extends Error {
constructor(
public statusCode: number,
public message: string,
public code?: string
) {
super(message)
}
}
export function errorHandler(err: Error, c: Context) {
console.error(err)
if (err instanceof AppError) {
return c.json({
error: err.message,
code: err.code,
}, err.statusCode as any)
}
if (err instanceof HTTPException) {
return c.json({
error: err.message,
}, err.status)
}
// Don't leak error details in production
return c.json({
error: process.env.NODE_ENV === 'production'
? 'Internal server error'
: err.message,
}, 500)
}
// src/lib/storage.ts
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
const s3 = new S3Client({
region: 'auto',
endpoint: process.env.R2_ENDPOINT,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
},
})
const BUCKET = process.env.R2_BUCKET!
export async function uploadFile(
key: string,
data: Buffer,
contentType: string
): Promise<string> {
await s3.send(new PutObjectCommand({
Bucket: BUCKET,
Key: key,
Body: data,
ContentType: contentType,
}))
return `${process.env.R2_PUBLIC_URL}/${key}`
}
export async function getSignedUploadUrl(
key: string,
contentType: string,
expiresIn = 3600
): Promise<string> {
const command = new PutObjectCommand({
Bucket: BUCKET,
Key: key,
ContentType: contentType,
})
return getSignedUrl(s3, command, { expiresIn })
}
export async function getSignedDownloadUrl(
key: string,
expiresIn = 3600
): Promise<string> {
const command = new GetObjectCommand({
Bucket: BUCKET,
Key: key,
})
return getSignedUrl(s3, command, { expiresIn })
}
// src/routes/uploads.ts
import { Hono } from 'hono'
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'
import { authMiddleware } from '../middleware/auth'
import { getSignedUploadUrl } from '../lib/storage'
import { nanoid } from 'nanoid'
export const uploadRoutes = new Hono()
uploadRoutes.use('*', authMiddleware)
const presignSchema = z.object({
contentType: z.string(),
filename: z.string(),
})
// Get presigned URL for upload
uploadRoutes.post('/presign', zValidator('json', presignSchema), async (c) => {
const userId = c.get('userId')
const { contentType, filename } = c.req.valid('json')
// Validate content type
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp']
if (!allowedTypes.includes(contentType)) {
return c.json({ error: 'Invalid content type' }, 400)
}
// Generate unique key
const ext = filename.split('.').pop()
const key = `uploads/${userId}/${nanoid()}.${ext}`
const uploadUrl = await getSignedUploadUrl(key, contentType)
const publicUrl = `${process.env.R2_PUBLIC_URL}/${key}`
return c.json({
uploadUrl,
publicUrl,
key,
})
})
# Server
PORT=3000
NODE_ENV=development
# Database
DATABASE_URL="postgresql://..."
# JWT
JWT_ACCESS_SECRET="your-access-secret-min-32-chars"
JWT_REFRESH_SECRET="your-refresh-secret-min-32-chars"
# Apple Sign In
APPLE_BUNDLE_ID="com.yourcompany.app"
# Cloudflare R2
R2_ENDPOINT="https://xxx.r2.cloudflarestorage.com"
R2_ACCESS_KEY_ID="..."
R2_SECRET_ACCESS_KEY="..."
R2_BUCKET="your-bucket"
R2_PUBLIC_URL="https://pub-xxx.r2.dev"
// src/routes/health.ts
import { Hono } from 'hono'
import { prisma } from '../lib/db'
export const healthRoutes = new Hono()
healthRoutes.get('/', async (c) => {
const checks = {
server: 'ok',
database: 'ok',
timestamp: new Date().toISOString(),
}
try {
await prisma.$queryRaw`SELECT 1`
} catch (error) {
checks.database = 'error'
}
const status = Object.values(checks).includes('error') ? 503 : 200
return c.json(checks, status)
})
healthRoutes.get('/ready', async (c) => {
try {
await prisma.$queryRaw`SELECT 1`
return c.json({ status: 'ready' })
} catch (error) {
return c.json({ status: 'not ready' }, 503)
}
})
# Dockerfile
FROM node:20-alpine AS base
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npx prisma generate
RUN npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/prisma ./prisma
EXPOSE 3000
CMD ["node", "dist/index.js"]
# railway.toml
[build]
builder = "nixpacks"
[deploy]
startCommand = "npx prisma migrate deploy && node dist/index.js"
healthcheckPath = "/health"
healthcheckTimeout = 100
□ HTTPS only (enforced at load balancer)
□ CORS configured for specific origins
□ Rate limiting on all endpoints
□ Input validation with Zod
□ SQL injection prevented (Prisma)
□ XSS prevented (JSON responses only)
□ Secrets in environment variables
□ JWT secrets are strong (32+ chars)
□ Refresh tokens hashed in database
□ Password hashing with bcrypt (12+ rounds)
□ Helmet headers enabled
□ Request size limits configured
development
Design principles for building polished, native-feeling SwiftUI apps and widgets. Use this skill when creating or modifying SwiftUI views, iOS widgets (WidgetKit), or any native Apple UI. Ensures proper spacing, typography, colors, and widget implementations that look and feel like quality apps rather than AI-generated slop.
data-ai
Design and implement SwiftUI views, components, and app architecture. Use when creating new SwiftUI views, implementing MVVM/TCA patterns, managing state with @Observable, @State, @Binding, or @Environment, designing navigation flows, or structuring iOS app architecture. Triggers on SwiftUI, view model, state management, navigation, coordinator pattern.
development
Implement, review, or improve SwiftUI animations and transitions. Use when adding implicit or explicit animations with withAnimation, configuring spring animations (.smooth, .snappy, .bouncy), building phase or keyframe animations with PhaseAnimator/KeyframeAnimator, creating hero transitions with matchedGeometryEffect or matchedTransitionSource, adding SF Symbol effects (bounce, pulse, variableColor, breathe, rotate, wiggle), implementing custom Transition or CustomAnimation types, or ensuring animations respect accessibilityReduceMotion.
testing
Audit SwiftUI views for accessibility (iOS + macOS) with patch-ready fixes