Files
property-management-network/.do/app.yaml
T
Leon SerfatyandClaude Opus 4.8 c9968531e4 Consolidate audit-fixes branch: webhooks, integrations, and deploy hardening
Batch commit of the pending working tree on security/audit-fixes-2026-07.
Major areas:
- Outbound webhooks / Zapier: schema + signed delivery with retries, public
  v1 API (REST-hook subscribe/unsubscribe), settings UI, cron drain.
- Deploy hardening: email via SMTP2GO (Resend fully removed), verified DB TLS
  (DATABASE_SSL=require + DATABASE_CA), storage fails loud in production when
  Spaces is unconfigured instead of silently using ephemeral disk.
- Integrations & features (concurrent work): accounting (QuickBooks/Xero),
  e-signature (DocuSign/Dropbox Sign), PayPal, geocoding/maps, onboarding,
  expanded legal pages.
- DB migrations 0006–0009.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:42:34 -04:00

173 lines
7.3 KiB
YAML

# ─────────────────────────────────────────────────────────────────────────────
# DigitalOcean App Platform spec — Property Management Network
#
# Deploy: doctl apps create --spec .do/app.yaml
# Update: doctl apps update <APP_ID> --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