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

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`).