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>
173 lines
7.3 KiB
YAML
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
|