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)
Container Registry ─── Google Artifact Registry
| Setting | Value |
|---|
| Provider | Neon |
| Version | PostgreSQL 16 |
| Region | Frankfurt (eu-central-1) |
| Plan | Free tier (0.5 GB storage, autoscaling compute) |
| Tables | 60 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.
| Setting | Value |
|---|
| Provider | Upstash |
| Region | EU-West-1 |
| Protocol | rediss:// (TLS required) |
| Used for | BullMQ job queues, circuit breaker state, rate limiter counters |
Connection string format:
rediss://default:<password>@<host>.upstash.io:6379
| Setting | Value |
|---|
| Provider | Google Cloud Run |
| Project | pinteach-api |
| Region | europe-west1 (Belgium) |
| Runtime | Bun (via oven/bun:1-slim Docker image) |
| Port | 8080 |
| Min instances | 1 (required for BullMQ background workers) |
| Max instances | 10 |
| Memory | 512 MiB |
| CPU | 1 |
| Domain | api.pinteach.com |
The API uses a 2-stage Docker build:
# Stage 1: Install + Build
FROM oven/bun:1 AS builder
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 run build --filter=@pinteach/api --filter=@pinteach/shared \
--filter=@pinteach/db --filter=@pinteach/email-templates
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 ./
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
# Set Python version for gcloud CLI (macOS)
export CLOUDSDK_PYTHON=/usr/local/bin/python3.12
gcloud builds submit --tag europe-west1-docker.pkg.dev/pinteach-api/pinteach/api
--image europe-west1-docker.pkg.dev/pinteach-api/pinteach/api \
--allow-unauthenticated \
| Variable | Description |
|---|
DATABASE_URL | Neon PostgreSQL connection string (pooler endpoint) |
REDIS_URL | Upstash Redis connection string (rediss://) |
APP_URL | https://app.pinteach.com |
SESSION_SECRET | Cookie signing secret (min 32 chars) |
GOOGLE_CLIENT_ID | Google OAuth client ID |
GOOGLE_CLIENT_SECRET | Google OAuth client secret |
GOOGLE_REDIRECT_URI | https://api.pinteach.com/api/auth/google/callback |
STRIPE_SECRET_KEY | Stripe platform secret key |
STRIPE_CONNECT_CLIENT_ID | Stripe Connect client ID |
STRIPE_WEBHOOK_SECRET | Stripe webhook signing secret |
RESEND_API_KEY | Resend email API key |
NODE_ENV | production |
PORT | 8080 |
HOST | 0.0.0.0 |
LOG_LEVEL | info |
api.pinteach.com is mapped via:
- Google Cloud Run domain mapping (
gcloud beta run domain-mappings create)
- Cloudflare DNS: CNAME
api → ghs.googlehosted.com (DNS only, not proxied)
- SSL certificate provisioned by Google (takes 15-20 min on first setup)
| Setting | Value |
|---|
| Provider | Cloudflare Pages |
| Framework | Astro 5 + @astrojs/cloudflare adapter |
| Package | @pinteach/web |
| Domain | pinteach.com |
| Field | Value |
|---|
| Framework preset | Astro |
| Build command | bun install && bun run build --filter=@pinteach/web |
| Build output directory | apps/web/dist |
| Variable | Value |
|---|
SKIP_DEPENDENCY_INSTALL | true |
NODE_VERSION | 22 |
API_URL | https://api.pinteach.com/api |
APP_URL | https://app.pinteach.com |
| Setting | Value |
|---|
| Provider | Cloudflare Pages |
| Framework | React 19 + Vite 6 (static SPA) |
| Package | @pinteach/app |
| Domain | app.pinteach.com |
| Field | Value |
|---|
| Framework preset | None |
| Build command | bun install && bun run build --filter=@pinteach/app |
| Build output directory | apps/app/dist |
| Variable | Value |
|---|
SKIP_DEPENDENCY_INSTALL | true |
NODE_VERSION | 22 |
VITE_API_URL | https://api.pinteach.com/api |
VITE_ASTRO_URL | https://pinteach.com |
| Setting | Value |
|---|
| Provider | Cloudflare Pages |
| Framework | Astro Starlight (static) |
| Package | @pinteach/platform |
| Domain | platform.pinteach.com |
| Field | Value |
|---|
| Framework preset | Astro |
| Build command | bun install && bun run build --filter=@pinteach/platform |
| Build output directory | apps/platform/dist |
| Variable | Value |
|---|
SKIP_DEPENDENCY_INSTALL | true |
NODE_VERSION | 22 |
All DNS records for pinteach.com:
| Type | Name | Target | Proxy |
|---|
| CNAME | api | ghs.googlehosted.com | DNS only (gray) |
| CNAME | app | <pages-project>.pages.dev | Proxied (orange) |
| CNAME | platform | <pages-project>.pages.dev | Proxied (orange) |
| CNAME | @ / root | <pages-project>.pages.dev | Proxied (orange) |
| Service | Plan | Estimated Cost |
|---|
| Neon PostgreSQL | Free → Launch ($19/mo) | $0 — $19/mo |
| Upstash Redis | Free (10K commands/day) | $0/mo |
| Google Cloud Run | 1 min instance, 512 MiB | ~$15 — $25/mo |
| Cloudflare Pages | Free (3 projects, unlimited bandwidth) | $0/mo |
| Google Artifact Registry | Container 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
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
--image europe-west1-docker.pkg.dev/pinteach-api/pinteach/api \
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
DATABASE_URL="<neon-connection-string>" bunx drizzle-kit migrate