Skip to content

Deployment

PinTeach runs on 4 services across 3 providers. All infrastructure is in EU regions for GDPR compliance.

pinteach.com ─── Cloudflare Pages (Astro SSR)
app.pinteach.com ─── Cloudflare Pages (React SPA, static)
api.pinteach.com ─── Google Cloud Run (Fastify + BullMQ)
platform.pinteach.com ─── Cloudflare Pages (Starlight docs, static)
PostgreSQL ─── Neon (serverless, Frankfurt)
Redis ─── Upstash (serverless, EU-West-1)
DNS + CDN ─── Cloudflare
Container Registry ─── Google Artifact Registry

SettingValue
ProviderNeon
VersionPostgreSQL 16
RegionFrankfurt (eu-central-1)
PlanFree tier (0.5 GB storage, autoscaling compute)
Tables60 tables, 53 enums

Connection string format:

postgresql://<user>:<password>@<host>-pooler.<region>.aws.neon.tech/<database>?sslmode=require

Database was initialized using drizzle-kit push --force instead of running migrations sequentially, since Neon starts with a clean database.

SettingValue
ProviderUpstash
RegionEU-West-1
Protocolrediss:// (TLS required)
Used forBullMQ job queues, circuit breaker state, rate limiter counters

Connection string format:

rediss://default:<password>@<host>.upstash.io:6379

SettingValue
ProviderGoogle Cloud Run
Projectpinteach-api
Regioneurope-west1 (Belgium)
RuntimeBun (via oven/bun:1-slim Docker image)
Port8080
Min instances1 (required for BullMQ background workers)
Max instances10
Memory512 MiB
CPU1
Domainapi.pinteach.com

The API uses a 2-stage Docker build:

# Stage 1: Install + Build
FROM oven/bun:1 AS builder
WORKDIR /app
COPY package.json bun.lockb turbo.json ./
COPY apps/api/package.json ./apps/api/
COPY packages/db/package.json ./packages/db/
COPY packages/shared/package.json ./packages/shared/
COPY packages/email-templates/package.json ./packages/email-templates/
RUN bun install
COPY apps/api ./apps/api
COPY packages ./packages
COPY tsconfig*.json ./
RUN bun run build --filter=@pinteach/api --filter=@pinteach/shared \
--filter=@pinteach/db --filter=@pinteach/email-templates
# Stage 2: Runtime
FROM oven/bun:1-slim
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/apps/api ./apps/api
COPY --from=builder /app/packages ./packages
COPY --from=builder /app/package.json ./
ENV PORT=8080
ENV HOST=0.0.0.0
EXPOSE 8080
CMD ["bun", "run", "apps/api/src/index.ts"]

Key details:

  • .dockerignore excludes apps/web, apps/platform, .claude, .env*, node_modules
  • Bun workspaces hoist all deps to root node_modules — no per-app node_modules directories
  • Only API + shared packages are copied to the runtime stage
Terminal window
# Set Python version for gcloud CLI (macOS)
export CLOUDSDK_PYTHON=/usr/local/bin/python3.12
# Build and deploy
gcloud builds submit --tag europe-west1-docker.pkg.dev/pinteach-api/pinteach/api
gcloud run deploy api \
--image europe-west1-docker.pkg.dev/pinteach-api/pinteach/api \
--region europe-west1 \
--platform managed \
--allow-unauthenticated \
--min-instances 1 \
--max-instances 10 \
--memory 512Mi \
--cpu 1 \
--port 8080
VariableDescription
DATABASE_URLNeon PostgreSQL connection string (pooler endpoint)
REDIS_URLUpstash Redis connection string (rediss://)
APP_URLhttps://app.pinteach.com
SESSION_SECRETCookie signing secret (min 32 chars)
GOOGLE_CLIENT_IDGoogle OAuth client ID
GOOGLE_CLIENT_SECRETGoogle OAuth client secret
GOOGLE_REDIRECT_URIhttps://api.pinteach.com/api/auth/google/callback
STRIPE_SECRET_KEYStripe platform secret key
STRIPE_CONNECT_CLIENT_IDStripe Connect client ID
STRIPE_WEBHOOK_SECRETStripe webhook signing secret
RESEND_API_KEYResend email API key
NODE_ENVproduction
PORT8080
HOST0.0.0.0
LOG_LEVELinfo

api.pinteach.com is mapped via:

  1. Google Cloud Run domain mapping (gcloud beta run domain-mappings create)
  2. Cloudflare DNS: CNAME apighs.googlehosted.com (DNS only, not proxied)
  3. SSL certificate provisioned by Google (takes 15-20 min on first setup)

Astro SSR — Cloudflare Pages (pinteach.com)

Section titled “Astro SSR — Cloudflare Pages (pinteach.com)”
SettingValue
ProviderCloudflare Pages
FrameworkAstro 5 + @astrojs/cloudflare adapter
Package@pinteach/web
Domainpinteach.com
FieldValue
Framework presetAstro
Build commandbun install && bun run build --filter=@pinteach/web
Build output directoryapps/web/dist
VariableValue
SKIP_DEPENDENCY_INSTALLtrue
NODE_VERSION22
API_URLhttps://api.pinteach.com/api
APP_URLhttps://app.pinteach.com

React SPA — Cloudflare Pages (app.pinteach.com)

Section titled “React SPA — Cloudflare Pages (app.pinteach.com)”
SettingValue
ProviderCloudflare Pages
FrameworkReact 19 + Vite 6 (static SPA)
Package@pinteach/app
Domainapp.pinteach.com
FieldValue
Framework presetNone
Build commandbun install && bun run build --filter=@pinteach/app
Build output directoryapps/app/dist
VariableValue
SKIP_DEPENDENCY_INSTALLtrue
NODE_VERSION22
VITE_API_URLhttps://api.pinteach.com/api
VITE_ASTRO_URLhttps://pinteach.com

Platform Docs — Cloudflare Pages (platform.pinteach.com)

Section titled “Platform Docs — Cloudflare Pages (platform.pinteach.com)”
SettingValue
ProviderCloudflare Pages
FrameworkAstro Starlight (static)
Package@pinteach/platform
Domainplatform.pinteach.com
FieldValue
Framework presetAstro
Build commandbun install && bun run build --filter=@pinteach/platform
Build output directoryapps/platform/dist
VariableValue
SKIP_DEPENDENCY_INSTALLtrue
NODE_VERSION22

All DNS records for pinteach.com:

TypeNameTargetProxy
CNAMEapighs.googlehosted.comDNS only (gray)
CNAMEapp<pages-project>.pages.devProxied (orange)
CNAMEplatform<pages-project>.pages.devProxied (orange)
CNAME@ / root<pages-project>.pages.devProxied (orange)

ServicePlanEstimated Cost
Neon PostgreSQLFree → Launch ($19/mo)$0 — $19/mo
Upstash RedisFree (10K commands/day)$0/mo
Google Cloud Run1 min instance, 512 MiB~$15 — $25/mo
Cloudflare PagesFree (3 projects, unlimited bandwidth)$0/mo
Google Artifact RegistryContainer storage~$1/mo
Total~$16 — $45/mo

Scaling notes:

  • Neon free tier includes 0.5 GB storage and 191 compute hours/month — enough for ~50 teachers
  • Cloud Run min-instances=1 is the main fixed cost (~$15/mo)
  • Cloudflare Pages has no bandwidth limits on the free plan
  • Upstash free tier supports 10K commands/day — sufficient for BullMQ with 11 workers

Terminal window
export CLOUDSDK_PYTHON=/usr/local/bin/python3.12
# Build + push Docker image
gcloud builds submit --tag europe-west1-docker.pkg.dev/pinteach-api/pinteach/api
# Deploy to Cloud Run
gcloud run deploy api \
--image europe-west1-docker.pkg.dev/pinteach-api/pinteach/api \
--region europe-west1

All 3 Cloudflare Pages projects auto-deploy on push to main:

  • pinteach.com — rebuilds @pinteach/web
  • app.pinteach.com — rebuilds @pinteach/app
  • platform.pinteach.com — rebuilds @pinteach/platform
Terminal window
# From packages/db/
DATABASE_URL="<neon-connection-string>" bunx drizzle-kit migrate