# ───────────────────────────────────────────────────────────────────────────── # DigitalOcean App Platform spec — Property Management Network # # Deploy: doctl apps create --spec .do/app.yaml # Update: doctl apps update --spec .do/app.yaml # # SOURCE: image-based from DigitalOcean Container Registry (DOCR). The app's git # lives on self-hosted Gitea, which App Platform cannot pull, so we build the # Docker image ourselves and push it to DOCR. See DIGITALOCEAN.md for the full # build/push/deploy walkthrough. # # SECRETS: values marked `type: SECRET` are placeholders — set the real values in # the App Platform dashboard (App → Settings → Environment Variables) or via # `doctl`. Never commit real secrets to this file. # ───────────────────────────────────────────────────────────────────────────── name: property-management-network region: nyc services: - name: web # Pre-built image pushed to DOCR (repository must exist in your registry). image: registry_type: DOCR repository: property-management-network tag: latest deploy_on_push: enabled: true instance_count: 1 instance_size_slug: apps-s-1vcpu-1gb http_port: 3000 health_check: http_path: /api/health initial_delay_seconds: 20 period_seconds: 30 timeout_seconds: 5 success_threshold: 1 failure_threshold: 3 envs: # ── App URLs ────────────────────────────────────────────────────────── # ${APP_URL} resolves to the app's public URL at runtime. NOTE: the client # bundle bakes NEXT_PUBLIC_APP_URL at *image build* time (see Dockerfile / # DIGITALOCEAN.md), so build the image with the same URL you serve on. - key: NEXT_PUBLIC_APP_URL scope: RUN_TIME value: ${APP_URL} - key: BETTER_AUTH_URL scope: RUN_TIME value: ${APP_URL} - key: NEXT_PUBLIC_APP_NAME scope: RUN_TIME value: Property Management Network # ── Database (managed Postgres — use the PRIVATE host; see DIGITALOCEAN.md) ── - key: DATABASE_URL scope: RUN_TIME type: SECRET value: REPLACE_IN_DASHBOARD # Verified TLS (encrypted + certificate-checked). DO Managed Postgres uses # a CA that isn't in the system trust store, so paste the cluster's CA cert # into DATABASE_CA: DO control panel → Database → Connection Details → # "Download CA certificate", then paste its PEM contents as the DATABASE_CA # secret in the App Platform dashboard. Without a valid CA the app will # refuse to connect (fail loud) rather than run unverified. # Emergency fallback ONLY (not for production): DATABASE_SSL=no-verify is # encrypted but does NOT verify the server certificate. - key: DATABASE_SSL scope: RUN_TIME value: require - key: DATABASE_CA scope: RUN_TIME type: SECRET value: REPLACE_IN_DASHBOARD # Schema is migrated out-of-band (as doadmin), NOT on boot — the app user # intentionally lacks DDL rights. Keep this false; run migrations manually. - key: RUN_MIGRATIONS_ON_START scope: RUN_TIME value: "false" # ── Better Auth ─────────────────────────────────────────────────────── - key: BETTER_AUTH_SECRET scope: RUN_TIME type: SECRET value: REPLACE_IN_DASHBOARD - key: GOOGLE_CLIENT_ID scope: RUN_TIME type: SECRET value: REPLACE_IN_DASHBOARD - key: GOOGLE_CLIENT_SECRET scope: RUN_TIME type: SECRET value: REPLACE_IN_DASHBOARD # ── Stripe ──────────────────────────────────────────────────────────── - key: STRIPE_SECRET_KEY scope: RUN_TIME type: SECRET value: REPLACE_IN_DASHBOARD - key: STRIPE_WEBHOOK_SECRET scope: RUN_TIME type: SECRET value: REPLACE_IN_DASHBOARD # No STRIPE_*_PRICE_ID vars — prices are resolved by lookup key and # auto-created on first checkout (lib/stripe/prices.ts). Going live only # needs the two live secrets above + the live publishable key below. # ── OpenAI ──────────────────────────────────────────────────────────── - key: OPENAI_API_KEY scope: RUN_TIME type: SECRET value: REPLACE_IN_DASHBOARD # ── Email (SMTP — SMTP2GO) ──────────────────────────────────────────── # The app sends mail via SMTP only (nodemailer). Email is silently skipped # unless SMTP_HOST + SMTP_USER + SMTP_PASS are all set — password resets, # email verification, rent/overdue/lease reminders, team invites, and # payment links all depend on this. EMAIL_FROM is a bare address; the app # wraps it as "Property Management Network <…>". - key: SMTP_HOST scope: RUN_TIME value: mail.smtp2go.com - key: SMTP_PORT scope: RUN_TIME value: "2525" - key: SMTP_USER scope: RUN_TIME type: SECRET value: REPLACE_IN_DASHBOARD - key: SMTP_PASS scope: RUN_TIME type: SECRET value: REPLACE_IN_DASHBOARD - key: EMAIL_FROM scope: RUN_TIME value: postmaster@propertymanagement.network # ── Cloudflare Turnstile (site key is public; baked into the client bundle # at image build time — keep it in sync when you build) ── - key: NEXT_PUBLIC_TURNSTILE_SITE_KEY scope: RUN_TIME value: 0x4AAAAAADuDQverznfv1a60 - key: TURNSTILE_SECRET_KEY scope: RUN_TIME type: SECRET value: REPLACE_IN_DASHBOARD # ── Object storage (DigitalOcean Spaces + CDN) ──────────────────────── - key: SPACES_KEY scope: RUN_TIME type: SECRET value: REPLACE_IN_DASHBOARD - key: SPACES_SECRET scope: RUN_TIME type: SECRET value: REPLACE_IN_DASHBOARD - key: SPACES_REGION scope: RUN_TIME value: nyc3 - key: SPACES_BUCKET scope: RUN_TIME value: property-management-network - key: SPACES_ENDPOINT scope: RUN_TIME value: https://nyc3.digitaloceanspaces.com - key: SPACES_CDN_ENDPOINT scope: RUN_TIME value: https://nyc3.cdn.digitaloceanspaces.com # ── Cron (Bearer token the DO Function sends to /api/cron/*) ── - key: CRON_SECRET scope: RUN_TIME type: SECRET value: REPLACE_IN_DASHBOARD