Files
property-management-network/COOLIFY.md
T
Leon SerfatyandClaude Opus 4.8 857b9a7811 Initial import: property management SaaS + security hardening + admin dashboard
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>
2026-06-23 20:36:07 -04:00

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.json crons).

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.


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_URL and NEXT_PUBLIC_APP_NAME are 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 via ARG/--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 to 3000 if 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=false and 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/health returns {"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/storage volume)
  • Trigger the daily scheduled task manually and confirm a 200 in logs
  • (If billing) Stripe webhook delivers successfully

Notes

  • Google OAuth: set GOOGLE_CLIENT_ID/SECRET and add <BETTER_AUTH_URL>/api/auth/callback/google as 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=disable for Coolify's internal private-network Postgres (and the bundled compose DB), require for managed/external DBs with a public CA, or no-verify for self-signed certs (optionally supply DATABASE_CA).