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>
128 lines
6.6 KiB
Markdown
128 lines
6.6 KiB
Markdown
# 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.**
|
|
|
|
---
|
|
|
|
## 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`](.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 `EXPOSE`d). 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`](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:
|
|
```sh
|
|
# 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`).
|