Property Management Network — Next.js 16 (App Router), Better Auth, Drizzle ORM over PostgreSQL, Stripe, OpenAI, Resend. Includes: - Security hardening: access-control/IDOR fixes, TLS-by-default DB layer, constant-time cron auth, strict security headers, atomic AI quota gating, HTML/email output encoding, demo-backdoor disabled in production. - Superadmin dashboard at /admin (overview/MRR, server-paginated users with ban/impersonate/plan/delete, billing, platform activity + admin audit log, AI usage, system health) via the Better Auth admin plugin. - Seed/migration utility scripts under scripts/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6.6 KiB
Deploying Property Management Network on Coolify
This app is a Next.js 16 (App Router) server that needs:
- a PostgreSQL database,
- a persistent volume for uploaded files (documents/photos are stored on local disk under
STORAGE_DIR), - a few third-party API keys (Stripe, OpenAI, Resend),
- scheduled tasks for the rent/lease cron jobs (Coolify replaces
vercel.jsoncrons).
The repo ships a production Dockerfile (standalone output), a /api/health liveness probe, and an entrypoint that runs database migrations on boot.
There are two ways to deploy. Path A (Dockerfile + separate Postgres) is recommended.
Path A — Dockerfile build pack + Coolify Postgres (recommended)
1. Create the database
In your Coolify project: + New → Database → PostgreSQL. Once created, copy its internal connection string (looks like postgres://postgres:PASSWORD@<service>:5432/postgres). Use the internal host — the app talks to it over Coolify's private network.
2. Create the application
+ New → Application → Public/Private Git Repository, point it at this repo, and set Build Pack = Dockerfile.
3. Set environment variables
Under the app's Environment Variables, add everything from .env.production.example. At minimum:
| Variable | Notes |
|---|---|
DATABASE_URL |
Internal Postgres URL from step 1. |
DATABASE_SSL |
TLS policy. Use disable for Coolify's internal/private-network Postgres; require (default) for managed/external DBs; no-verify for self-signed certs. |
BETTER_AUTH_SECRET |
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" |
BETTER_AUTH_URL |
Your public URL, e.g. https://propertymanagement.network |
NEXT_PUBLIC_APP_URL |
Same public URL. Also mark as a Build Variable (see below). |
NEXT_PUBLIC_APP_NAME |
Property Management Network (Build Variable too). |
CRON_SECRET |
Random string; protects the cron endpoints. |
RESEND_API_KEY, RESEND_FROM_EMAIL |
Email sending. |
STRIPE_* |
Billing (optional to start). |
OPENAI_API_KEY |
AI assistant (optional to start). |
Build Variables:
NEXT_PUBLIC_APP_URLandNEXT_PUBLIC_APP_NAMEare inlined into the browser bundle at build time. In Coolify, set them so they're available at build (toggle "Build Variable" / "Available at Buildtime"). They're passed to the image viaARG/--build-arg.
4. Add a persistent volume for uploads
Uploaded files are written to STORAGE_DIR (default /app/storage). Without a volume they're lost on every redeploy.
Under the app's Storages → Add: mount a persistent volume at the container path /app/storage.
5. Domain & port
- Set the app's Domain to your URL; Coolify provisions HTTPS automatically.
- The container listens on port 3000 (already
EXPOSEd). Coolify usually detects this; set the port to3000if asked.
6. Health check
The image has a built-in Docker HEALTHCHECK hitting /api/health. You can also set Coolify's health check path to /api/health.
7. Deploy
Click Deploy. On boot the entrypoint runs scripts/migrate.mjs to apply migrations, then starts the server. Watch the deploy logs for [migrate] Migrations applied successfully. followed by the Next.js ready line.
Path B — Docker Compose (app + Postgres bundled)
Use the included docker-compose.yml with Coolify's Docker Compose build pack. It defines the app and a db (Postgres 17) plus named volumes app-storage and db-data.
Set these env vars in Coolify (mark NEXT_PUBLIC_* and POSTGRES_* as available at build time):
POSTGRES_USER=pmn
POSTGRES_PASSWORD=<strong-password>
POSTGRES_DB=pmn
BETTER_AUTH_SECRET=<hex>
BETTER_AUTH_URL=https://your-domain
NEXT_PUBLIC_APP_URL=https://your-domain
NEXT_PUBLIC_APP_NAME=Property Management Network
CRON_SECRET=<random>
RESEND_API_KEY=... # plus STRIPE_*, OPENAI_API_KEY as needed
DATABASE_URL is composed automatically from the POSTGRES_* values inside the compose file. The app waits for the DB healthcheck before starting and migrations retry while Postgres comes up.
Database migrations
Migrations live in lib/db/migrations (Drizzle). They run automatically on container start via the entrypoint.
- To disable auto-migrate (e.g. when running more than one replica), set
RUN_MIGRATIONS_ON_START=falseand run them as a one-off instead:# From a Coolify terminal/exec into the container: node scripts/migrate.mjs
Scheduled tasks (cron)
Coolify does not read vercel.json. Recreate the two jobs under the app's Scheduled Tasks. Each runs a command inside the container; authenticate with the CRON_SECRET env var that's already present there.
| Name | Schedule (UTC) | Command |
|---|---|---|
| Daily (rent reminders, overdue, lease expiry) | 0 9 * * * |
wget -q -O- --header="Authorization: Bearer $CRON_SECRET" http://127.0.0.1:3000/api/cron/daily |
| Late fees | 0 8 * * * |
wget -q -O- --header="Authorization: Bearer $CRON_SECRET" http://127.0.0.1:3000/api/cron/late-fees |
(The daily route already combines rent reminders, overdue marking, and 60/30/7-day lease-expiry emails.)
Stripe webhook (if using billing)
Point a Stripe webhook at https://<your-domain>/api/stripe/webhook and put its signing secret in STRIPE_WEBHOOK_SECRET. Subscribe to: checkout.session.completed, customer.subscription.created/updated/deleted, invoice.payment_failed, payment_intent.succeeded.
Post-deploy checklist
https://<domain>/api/healthreturns{"status":"ok",...}- Home page shows Property Management Network branding
- Sign up / log in works (verifies
DATABASE_URL+BETTER_AUTH_*) - Upload a document, redeploy, confirm it persists (verifies the
/app/storagevolume) - Trigger the
dailyscheduled task manually and confirm a 200 in logs - (If billing) Stripe webhook delivers successfully
Notes
- Google OAuth: set
GOOGLE_CLIENT_ID/SECRETand add<BETTER_AUTH_URL>/api/auth/callback/googleas an authorized redirect URI. - Scaling: with more than one replica, disable per-instance auto-migration (
RUN_MIGRATIONS_ON_START=false) and note that local-disk storage is per-container — move uploads to object storage (e.g. S3) if you scale horizontally. - TLS: the app and migrator default to encrypted + certificate-verified Postgres connections. Set
DATABASE_SSL=disablefor Coolify's internal private-network Postgres (and the bundled compose DB),requirefor managed/external DBs with a public CA, orno-verifyfor self-signed certs (optionally supplyDATABASE_CA).