Vibecode Supabase Pro
track this build5 phases, 15 steps, beginner friendly0%The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Supabase Pro, self-host a Postgres backend with auth, storage, and a small API surface. The hard boundary is managed postgres, backups, auth delivery, storage, edge functions, observability, and support, plus infrastructure scale, operations, and reliability.
You are building a lean indie version of Supabase Pro. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Supabase Pro · indie build The official Supabase stack running on one server behind TLS, with generated secrets rather than the shipped placeholders, Studio locked away from the public internet, nightly database and storage backups going off the machine, and a restore you have performed on a second server with the date recorded. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Platform | The official supabase/docker Compose stack | thirteen services that already work together; assembling them yourself is a month and a worse result | | Host | One Linux server with at least 4 GB of memory and 2 virtual CPUs | the documented floor; 8 GB is the realistic figure once you have data and any traffic | | Container runtime | Docker Engine 20.10 or newer with Compose v2 | what the stack's compose file expects | | Gateway | Kong, which the stack already includes, listening on port 8000 | one entry point in front of PostgREST, auth, storage and realtime | | Edge | Caddy in front of Kong for certificates and hostnames | automatic TLS, and a place to refuse traffic to Studio | | Backups | pg_dump plus a copy of the storage volume, sent off the machine | the database and the uploaded files are two separate things and a dump contains only one of them | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A Linux server with 4 GB of memory and 2 virtual CPUs** · roughly 20 to 40 USD a month at 8 GB with most providers - Why: Below this the stack starts and then dies under any real use. 8 GB is the honest recommendation once you have data. - Get it: Any virtual server provider. Choose a region near your users and a plan you can resize later. - Verify: free -m reports 4 GB or more and docker runs - [ ] **A second server for the restore drill** · a few USD if you delete it the same day - Why: The drill is worthless on the machine that already has your state. You can destroy this one afterwards. - Get it: The same provider, the smallest plan that meets the memory floor. Create it for the drill and delete it after. - Verify: You can SSH to it and run Docker - [ ] **A domain name with DNS you control** · roughly 10 to 15 USD a year - Why: Certificates need a hostname, and the API needs a stable address. - Get it: Any registrar. Add an A record for the API hostname pointing at the server. - Verify: dig your hostname returns the server address - [ ] **An off-server backup destination** · a few USD a month at these volumes; the first gigabytes are often free - Why: Backups on the server die with the server, which is the exact scenario you are protecting against. - Get it: Object storage from any provider, or a second host you control. Create a bucket and credentials scoped to write only into it. - Verify: A test file uploads and can be listed from your laptop - [ ] **A place to keep the generated secrets** · free - Why: The JWT secret is part of a working restore. Losing it means existing tokens and accounts do not survive. - Get it: A password manager entry, or an encrypted file kept somewhere other than the server. Record the anon key, service role key, JWT secret, database password and dashboard password. - Verify: You can retrieve all five from somewhere that is not the server ## Quick start ```sh adduser deploy && usermod -aG sudo deploy systemctl status ssh ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - A global edge network and the latency that comes with one. - Managed database operations, patching, tuning and failover. - Autoscaling, read replicas and connection pooling at scale. - Point in time recovery. Nightly dumps mean a bad day can cost you a day. - Anyone to call when it breaks. - managed Postgres, backups, auth delivery, storage, edge functions, observability, and support - global edge network - managed databases - autoscaling - DDoS protection, support, and compliance If one of those is essential to you, that is the reason to keep paying for Supabase Pro, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Supabase Pro The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a self-hosted Supabase stack on one server you own, with backups you have actually restored to replace Supabase Pro. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Do not write a backend as a service. Supabase publishes its own Docker Compose stack and it is the thing to deploy. This is an operations task, not a build task: every phase below is deploy, configure and verify. If you find yourself writing an auth server or a storage API, stop and go back to the compose file. The last phase is a restore drill and it must actually be performed, with the date written down. A backup you have never restored is a belief, not a backup. ### Stack (fixed, do not substitute) - Platform: The official supabase/docker Compose stack · thirteen services that already work together; assembling them yourself is a month and a worse result - Host: One Linux server with at least 4 GB of memory and 2 virtual CPUs · the documented floor; 8 GB is the realistic figure once you have data and any traffic - Container runtime: Docker Engine 20.10 or newer with Compose v2 · what the stack's compose file expects - Gateway: Kong, which the stack already includes, listening on port 8000 · one entry point in front of PostgREST, auth, storage and realtime - Edge: Caddy in front of Kong for certificates and hostnames · automatic TLS, and a place to refuse traffic to Studio - Backups: pg_dump plus a copy of the storage volume, sent off the machine · the database and the uploaded files are two separate things and a dump contains only one of them ### Data model (create this before Phase 1) - You are not designing a schema here. What you are designing is the operational state, and it is worth writing down before you start. - secrets · POSTGRES_PASSWORD, JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY, DASHBOARD_PASSWORD, all generated by you and none of them the shipped example values - volumes · the PostgreSQL data directory and the storage volume, which are the two things whose loss is unrecoverable - backups · dated database dumps and dated storage archives, in a destination that is not this server - restore log · the date of every restore drill you have performed, kept in the README ### Phase 1 · Server, Docker and a closed front door Build: Provision a server with at least 4 GB of memory. Install Docker Engine and the compose plugin. Create a non-root user. Configure the firewall to allow only SSH and the web ports, and confirm from another machine that the database port is not reachable. Trap: Docker publishes ports by writing iptables rules that bypass the usual firewall front end, so a service you thought was blocked can be wide open. Verify with a port scan from a different machine rather than by reading your firewall configuration. Done when: Docker runs a test container as the non-root user. A port scan from another machine shows only SSH and the web ports open. Port 5432 is not reachable from outside the server. Do not build yet: No Supabase yet. A correctly closed server first. ### Phase 2 · The stack up, with secrets that are yours Build: Clone the supabase/docker directory, copy the example environment file, and replace every secret with a generated value. Generate the anon and service role keys from your own JWT secret. Bring the stack up and confirm every service reports healthy. Trap: The single most common self-hosting mistake is leaving the example values in place. The shipped anon key, service role key, JWT secret and dashboard password are published in the repository, so a stack running with them is readable and writable by anyone who finds it. Generate all of them before the first time the port is reachable, not after. Done when: Every container reports healthy. Grepping the environment file for the published example values returns nothing. A request signed with the example anon key is rejected. The service role key is not present in any file outside the environment file and your secret store. Do not build yet: No TLS and no public hostname yet. Keep it bound to localhost until the next phase. ### Phase 3 · TLS, hostnames and a private Studio Build: Point a domain at the server. Put Caddy in front of Kong so certificates are automatic. Expose the API host publicly. Do not expose Studio: bind it to localhost and reach it through an SSH tunnel, or put it behind an allowlist and its own authentication. Trap: Studio talks to the stack with the service role key, which bypasses every row level security policy you will ever write. Exposing it to the internet behind only a password makes that password equivalent to the whole database. Done when: The API answers over HTTPS with a valid certificate. Studio is not reachable from another machine, verified by trying. Studio is reachable through the tunnel and can list tables. A plain HTTP request is redirected to HTTPS. Do not build yet: No backups yet, which is the next phase and the important one. ### Phase 4 · Backups of both things that matter Build: A nightly pg_dump of the database and a nightly archive of the storage volume, both sent to a destination that is not this server, both dated, with several generations kept. Alert if a night passes with no new backup. Trap: A database dump does not contain uploaded files, and a storage archive does not contain the database. Backing up only one gives you a restore that produces rows pointing at files that are gone, or files nobody can find. Back up both, on the same schedule, and restore both in the drill. Done when: Two dated artefacts appear off the server each night. The database dump can be listed and its size is plausible. Deleting the newest dump and waiting a night produces a replacement and no alert; suppressing the job for a night produces an alert. Do not build yet: Do not call this done until Phase 5 has actually restored these artefacts. ### Phase 5 · The restore drill, actually performed Build: Provision a second server. Bring up a fresh stack. Load the newest database dump and unpack the newest storage archive. Verify row counts and that a stored file downloads. Write the date, the duration and anything that surprised you in the README. Trap: Restoring onto the same server proves almost nothing, because the parts you forgot are still sitting there. The drill has value precisely because the second machine has none of your undocumented state. Done when: A second server serves the same data. Row counts for the largest tables match the source. A file uploaded before the backup downloads correctly from the restored stack. Authentication works, meaning the JWT secret was part of what you restored. The date and duration are in the README. Do not build yet: Do not treat monitoring as a substitute for having done this. ### Phase 6 · Monitoring, upgrades and the incident you will have Build: Uptime checks against the API and the database. Disk alerts, because a full disk is the most common way this stack stops. A documented upgrade procedure that takes a backup first. Log retention that does not itself fill the disk. Trap: The stack's containers are versioned together and upgrading one service in isolation is how you get a database that the rest of the stack cannot talk to. Upgrade by moving the whole compose file forward, after a backup, and read the release notes for migration steps. Done when: An uptime check alerts within minutes when the API stops answering, tested by stopping it. A disk alert fires at a threshold that leaves room to act, tested by filling a scratch file. An upgrade has been performed on the second server first and only then on the live one. Do not build yet: Nothing after this. ### Out of scope (and why) - The global edge network. Your stack is on one machine in one place, and a visitor on another continent will feel that. - Managed database operations. Patching, tuning, connection limits and failover are now yours, and they are the largest part of what the subscription buys. - Autoscaling and read replicas. One server does not scale sideways, and pretending otherwise is how outages happen. - Point in time recovery. Nightly dumps mean you can lose up to a day; managed plans offer far tighter recovery windows and that difference is real. - Support. When something breaks at two in the morning, the person on call is you. ### README must contain - The server specification and why 4 GB is a floor rather than a target - That every shipped example secret was replaced, and where the real ones are stored - That Studio is not publicly reachable and exactly how to reach it - The backup schedule, both artefacts, the destination and the retention - The date and duration of the last restore drill, and the upgrade procedure ===== AGENTS.md ===== # Agent instructions · Supabase Pro indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: The official supabase/docker Compose stack, One Linux server with at least 4 GB of memory and 2 virtual CPUs, Docker Engine 20.10 or newer with Compose v2, Kong, which the stack already includes, listening on port 8000, Caddy in front of Kong for certificates and hostnames, pg_dump plus a copy of the storage volume, sent off the machine. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Docker publishes ports by writing iptables rules that bypass the usual firewall front end, so a service you thought was blocked can be wide open. Verify with a port scan from a different machine rather than by reading your firewall configuration. - The single most common self-hosting mistake is leaving the example values in place. The shipped anon key, service role key, JWT secret and dashboard password are published in the repository, so a stack running with them is readable and writable by anyone who finds it. Generate all of them before the first time the port is reachable, not after. - Studio talks to the stack with the service role key, which bypasses every row level security policy you will ever write. Exposing it to the internet behind only a password makes that password equivalent to the whole database. - A database dump does not contain uploaded files, and a storage archive does not contain the database. Backing up only one gives you a restore that produces rows pointing at files that are gone, or files nobody can find. Back up both, on the same schedule, and restore both in the drill. - Restoring onto the same server proves almost nothing, because the parts you forgot are still sitting there. The drill has value precisely because the second machine has none of your undocumented state. ===== BUILD_PLAN.md ===== # Build plan · Supabase Pro The official Supabase stack running on one server behind TLS, with generated secrets rather than the shipped placeholders, Studio locked away from the public internet, nightly database and storage backups going off the machine, and a restore you have performed on a second server with the date recorded. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Server, Docker and a closed front door A server exists, runs containers, and exposes nothing to the internet that you have not decided to expose. ### Steps 1. Provision and harden the server Create a non-root user with sudo, disable password authentication for SSH, and enable unattended security updates. ```sh adduser deploy && usermod -aG sudo deploy systemctl status ssh ``` 2. Install Docker Engine and Compose v2 Use Docker's own repository rather than a distribution package, which is often several versions behind. ```sh docker --version docker compose version ``` 3. Close the firewall and verify from outside Allow SSH, 80 and 443 only. Then scan from your laptop. Trusting the configuration file is how people expose databases. ```sh ufw allow OpenSSH && ufw allow 80,443/tcp && ufw enable nmap -Pn YOUR_SERVER_IP ``` ### Done when - [ ] docker compose version reports v2 or higher - [ ] A scan from another machine shows only the ports you intended - [ ] Port 5432 is unreachable from outside the server - [ ] SSH password authentication is disabled ### Watch out - Docker publishes ports by writing iptables rules that bypass the usual firewall front end, so a service you thought was blocked can be wide open. Verify with a port scan from a different machine rather than by reading your firewall configuration. ## Phase 2 · The stack up, with secrets that are yours Supabase runs, and not one credential is the value that shipped in the example file. ### Steps 1. Clone the stack and prepare the environment file Take only the docker directory. Copy .env.example to .env, then treat every line of it as something you must replace. Files: `docker/.env` ```sh git clone --depth 1 https://github.com/supabase/supabase cp supabase/docker/.env.example supabase/docker/.env ``` 2. Generate every secret A long random PostgreSQL password, a long random JWT secret, then derive the anon and service role keys by signing the documented payloads with that secret. Store them in a password manager as you go. ```sh openssl rand -base64 48 ``` 3. Bring it up and check health Start the stack and read the health of every container. A container in a restart loop is the normal first outcome and the logs say why. ```sh docker compose up -d docker compose ps ``` ### Done when - [ ] Every container reports healthy rather than restarting - [ ] No published example secret appears anywhere in the environment file - [ ] A request using the example anon key is rejected - [ ] The stack survives a full server reboot and comes back healthy ### Watch out - The single most common self-hosting mistake is leaving the example values in place. The shipped anon key, service role key, JWT secret and dashboard password are published in the repository, so a stack running with them is readable and writable by anyone who finds it. Generate all of them before the first time the port is reachable, not after. ## Phase 3 · TLS, hostnames and a private Studio The API is reachable over HTTPS at your domain, and the dashboard is not reachable by the public at all. ### Steps 1. Point DNS and run Caddy in front of Kong An A record for the API hostname. Caddy proxies to Kong on 8000 and obtains certificates on first request. Files: `Caddyfile` 2. Keep Studio off the public internet Bind its published port to 127.0.0.1 in the compose override, then reach it with ssh -L when you need it. Files: `docker-compose.override.yml` ```sh ssh -L 3000:localhost:3000 deploy@YOUR_SERVER ``` 3. Verify from outside, not from the server Curl the API host from your laptop, and try the Studio port from your laptop too. The second one must fail. ```sh curl -I https://api.example.com/rest/v1/ ``` ### Done when - [ ] The API answers over HTTPS with a valid certificate - [ ] The Studio port is refused from another machine - [ ] Studio works through an SSH tunnel and can list tables - [ ] HTTP redirects to HTTPS ### Watch out - Studio talks to the stack with the service role key, which bypasses every row level security policy you will ever write. Exposing it to the internet behind only a password makes that password equivalent to the whole database. ## Phase 4 · Backups of both things that matter The database and the uploaded files both leave the server on a schedule. ### Steps 1. Dump the database on a schedule pg_dump from a container or from the host, gzipped and dated, run by cron or a systemd timer. ```sh docker compose exec -T db pg_dump -U postgres postgres | gzip > db-$(date +%F).sql.gz ``` 2. Archive the storage volume Tar the storage volume to the same destination with the same date stamp so the two halves of a restore are obviously paired. ```sh tar czf storage-$(date +%F).tar.gz -C ./volumes storage ``` 3. Ship it off the machine and alert on absence Copy to object storage or another host. Then check each morning that a file with today's date exists, and alert when it does not. Files: `scripts/backup.sh`, `scripts/check_backup.sh` ### Done when - [ ] A dated database dump and a dated storage archive both land off the server nightly - [ ] The dump is a valid gzip and its size is plausible for the data volume - [ ] A missing backup produces an alert - [ ] Several generations are retained rather than one file being overwritten ### Watch out - A database dump does not contain uploaded files, and a storage archive does not contain the database. Backing up only one gives you a restore that produces rows pointing at files that are gone, or files nobody can find. Back up both, on the same schedule, and restore both in the drill. ## Phase 5 · The restore drill, actually performed You have brought the whole thing back on a different server, and you know how long it took. ### Steps 1. Stand up a clean second server Same Docker setup, fresh clone of the stack, and the same environment file, which means the secrets are part of your backup and not only in your head. 2. Restore both halves Load the dump into the fresh database and unpack the storage archive into the storage volume before starting the storage service. ```sh gunzip -c db-2026-09-07.sql.gz | docker compose exec -T db psql -U postgres postgres ``` 3. Verify and write it down Compare row counts, download a file that existed before the backup, and sign in with an existing account. Then record the date and how long it took. Files: `README.md` ### Done when - [ ] Row counts on the restored server match the source - [ ] A file that existed before the backup downloads from the restored stack - [ ] An existing user can authenticate against the restored stack - [ ] The README records the date and duration of this drill ### Watch out - Restoring onto the same server proves almost nothing, because the parts you forgot are still sitting there. The drill has value precisely because the second machine has none of your undocumented state. ## Not in this build - A global edge network and the latency that comes with one. - Managed database operations, patching, tuning and failover. - Autoscaling, read replicas and connection pooling at scale. - Point in time recovery. Nightly dumps mean a bad day can cost you a day. - Anyone to call when it breaks. ## After v1, if you want it - Point in time recovery with continuous archiving, once nightly dumps stop feeling sufficient - A staging stack restored from last night's backup, which also exercises the restore weekly - Read replicas, but only after measuring that one server is genuinely the limit ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Generate with openssl rand. Never the shipped example. POSTGRES_PASSWORD=generate-a-long-random-value # Required · secret. Generate it, then derive the anon and service role keys from it. It is part of a working restore, so back it up. JWT_SECRET=generate-a-long-random-value # Required. Signed with your JWT secret. Public by design, but it must be yours rather than the published example. ANON_KEY=eyJhbGciOi... # Required · secret. Signed with your JWT secret. Bypasses row level security entirely, so it never reaches a browser. SERVICE_ROLE_KEY=eyJhbGciOi... # Required · secret. Studio's own login. Change it even though Studio is not publicly exposed. DASHBOARD_PASSWORD=generate-a-long-random-value # Required. Where auth redirects send users after sign-in. SITE_URL=https://app.example.com # Required. The off-server destination for both nightly artefacts. BACKUP_DEST=s3://your-bucket/supabase
You are building a lean indie version of Supabase Pro. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Supabase Pro · indie build The official Supabase stack running on one server behind TLS, with generated secrets rather than the shipped placeholders, Studio locked away from the public internet, nightly database and storage backups going off the machine, and a restore you have performed on a second server with the date recorded. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Platform | The official supabase/docker Compose stack | thirteen services that already work together; assembling them yourself is a month and a worse result | | Host | One Linux server with at least 4 GB of memory and 2 virtual CPUs | the documented floor; 8 GB is the realistic figure once you have data and any traffic | | Container runtime | Docker Engine 20.10 or newer with Compose v2 | what the stack's compose file expects | | Gateway | Kong, which the stack already includes, listening on port 8000 | one entry point in front of PostgREST, auth, storage and realtime | | Edge | Caddy in front of Kong for certificates and hostnames | automatic TLS, and a place to refuse traffic to Studio | | Backups | pg_dump plus a copy of the storage volume, sent off the machine | the database and the uploaded files are two separate things and a dump contains only one of them | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A Linux server with 4 GB of memory and 2 virtual CPUs** · roughly 20 to 40 USD a month at 8 GB with most providers - Why: Below this the stack starts and then dies under any real use. 8 GB is the honest recommendation once you have data. - Get it: Any virtual server provider. Choose a region near your users and a plan you can resize later. - Verify: free -m reports 4 GB or more and docker runs - [ ] **A second server for the restore drill** · a few USD if you delete it the same day - Why: The drill is worthless on the machine that already has your state. You can destroy this one afterwards. - Get it: The same provider, the smallest plan that meets the memory floor. Create it for the drill and delete it after. - Verify: You can SSH to it and run Docker - [ ] **A domain name with DNS you control** · roughly 10 to 15 USD a year - Why: Certificates need a hostname, and the API needs a stable address. - Get it: Any registrar. Add an A record for the API hostname pointing at the server. - Verify: dig your hostname returns the server address - [ ] **An off-server backup destination** · a few USD a month at these volumes; the first gigabytes are often free - Why: Backups on the server die with the server, which is the exact scenario you are protecting against. - Get it: Object storage from any provider, or a second host you control. Create a bucket and credentials scoped to write only into it. - Verify: A test file uploads and can be listed from your laptop - [ ] **A place to keep the generated secrets** · free - Why: The JWT secret is part of a working restore. Losing it means existing tokens and accounts do not survive. - Get it: A password manager entry, or an encrypted file kept somewhere other than the server. Record the anon key, service role key, JWT secret, database password and dashboard password. - Verify: You can retrieve all five from somewhere that is not the server ## Quick start ```sh adduser deploy && usermod -aG sudo deploy systemctl status ssh ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - A global edge network and the latency that comes with one. - Managed database operations, patching, tuning and failover. - Autoscaling, read replicas and connection pooling at scale. - Point in time recovery. Nightly dumps mean a bad day can cost you a day. - Anyone to call when it breaks. - managed Postgres, backups, auth delivery, storage, edge functions, observability, and support - global edge network - managed databases - autoscaling - DDoS protection, support, and compliance If one of those is essential to you, that is the reason to keep paying for Supabase Pro, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Supabase Pro The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a self-hosted Supabase stack on one server you own, with backups you have actually restored to replace Supabase Pro. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Do not write a backend as a service. Supabase publishes its own Docker Compose stack and it is the thing to deploy. This is an operations task, not a build task: every phase below is deploy, configure and verify. If you find yourself writing an auth server or a storage API, stop and go back to the compose file. The last phase is a restore drill and it must actually be performed, with the date written down. A backup you have never restored is a belief, not a backup. ### Stack (fixed, do not substitute) - Platform: The official supabase/docker Compose stack · thirteen services that already work together; assembling them yourself is a month and a worse result - Host: One Linux server with at least 4 GB of memory and 2 virtual CPUs · the documented floor; 8 GB is the realistic figure once you have data and any traffic - Container runtime: Docker Engine 20.10 or newer with Compose v2 · what the stack's compose file expects - Gateway: Kong, which the stack already includes, listening on port 8000 · one entry point in front of PostgREST, auth, storage and realtime - Edge: Caddy in front of Kong for certificates and hostnames · automatic TLS, and a place to refuse traffic to Studio - Backups: pg_dump plus a copy of the storage volume, sent off the machine · the database and the uploaded files are two separate things and a dump contains only one of them ### Data model (create this before Phase 1) - You are not designing a schema here. What you are designing is the operational state, and it is worth writing down before you start. - secrets · POSTGRES_PASSWORD, JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY, DASHBOARD_PASSWORD, all generated by you and none of them the shipped example values - volumes · the PostgreSQL data directory and the storage volume, which are the two things whose loss is unrecoverable - backups · dated database dumps and dated storage archives, in a destination that is not this server - restore log · the date of every restore drill you have performed, kept in the README ### Phase 1 · Server, Docker and a closed front door Build: Provision a server with at least 4 GB of memory. Install Docker Engine and the compose plugin. Create a non-root user. Configure the firewall to allow only SSH and the web ports, and confirm from another machine that the database port is not reachable. Trap: Docker publishes ports by writing iptables rules that bypass the usual firewall front end, so a service you thought was blocked can be wide open. Verify with a port scan from a different machine rather than by reading your firewall configuration. Done when: Docker runs a test container as the non-root user. A port scan from another machine shows only SSH and the web ports open. Port 5432 is not reachable from outside the server. Do not build yet: No Supabase yet. A correctly closed server first. ### Phase 2 · The stack up, with secrets that are yours Build: Clone the supabase/docker directory, copy the example environment file, and replace every secret with a generated value. Generate the anon and service role keys from your own JWT secret. Bring the stack up and confirm every service reports healthy. Trap: The single most common self-hosting mistake is leaving the example values in place. The shipped anon key, service role key, JWT secret and dashboard password are published in the repository, so a stack running with them is readable and writable by anyone who finds it. Generate all of them before the first time the port is reachable, not after. Done when: Every container reports healthy. Grepping the environment file for the published example values returns nothing. A request signed with the example anon key is rejected. The service role key is not present in any file outside the environment file and your secret store. Do not build yet: No TLS and no public hostname yet. Keep it bound to localhost until the next phase. ### Phase 3 · TLS, hostnames and a private Studio Build: Point a domain at the server. Put Caddy in front of Kong so certificates are automatic. Expose the API host publicly. Do not expose Studio: bind it to localhost and reach it through an SSH tunnel, or put it behind an allowlist and its own authentication. Trap: Studio talks to the stack with the service role key, which bypasses every row level security policy you will ever write. Exposing it to the internet behind only a password makes that password equivalent to the whole database. Done when: The API answers over HTTPS with a valid certificate. Studio is not reachable from another machine, verified by trying. Studio is reachable through the tunnel and can list tables. A plain HTTP request is redirected to HTTPS. Do not build yet: No backups yet, which is the next phase and the important one. ### Phase 4 · Backups of both things that matter Build: A nightly pg_dump of the database and a nightly archive of the storage volume, both sent to a destination that is not this server, both dated, with several generations kept. Alert if a night passes with no new backup. Trap: A database dump does not contain uploaded files, and a storage archive does not contain the database. Backing up only one gives you a restore that produces rows pointing at files that are gone, or files nobody can find. Back up both, on the same schedule, and restore both in the drill. Done when: Two dated artefacts appear off the server each night. The database dump can be listed and its size is plausible. Deleting the newest dump and waiting a night produces a replacement and no alert; suppressing the job for a night produces an alert. Do not build yet: Do not call this done until Phase 5 has actually restored these artefacts. ### Phase 5 · The restore drill, actually performed Build: Provision a second server. Bring up a fresh stack. Load the newest database dump and unpack the newest storage archive. Verify row counts and that a stored file downloads. Write the date, the duration and anything that surprised you in the README. Trap: Restoring onto the same server proves almost nothing, because the parts you forgot are still sitting there. The drill has value precisely because the second machine has none of your undocumented state. Done when: A second server serves the same data. Row counts for the largest tables match the source. A file uploaded before the backup downloads correctly from the restored stack. Authentication works, meaning the JWT secret was part of what you restored. The date and duration are in the README. Do not build yet: Do not treat monitoring as a substitute for having done this. ### Phase 6 · Monitoring, upgrades and the incident you will have Build: Uptime checks against the API and the database. Disk alerts, because a full disk is the most common way this stack stops. A documented upgrade procedure that takes a backup first. Log retention that does not itself fill the disk. Trap: The stack's containers are versioned together and upgrading one service in isolation is how you get a database that the rest of the stack cannot talk to. Upgrade by moving the whole compose file forward, after a backup, and read the release notes for migration steps. Done when: An uptime check alerts within minutes when the API stops answering, tested by stopping it. A disk alert fires at a threshold that leaves room to act, tested by filling a scratch file. An upgrade has been performed on the second server first and only then on the live one. Do not build yet: Nothing after this. ### Out of scope (and why) - The global edge network. Your stack is on one machine in one place, and a visitor on another continent will feel that. - Managed database operations. Patching, tuning, connection limits and failover are now yours, and they are the largest part of what the subscription buys. - Autoscaling and read replicas. One server does not scale sideways, and pretending otherwise is how outages happen. - Point in time recovery. Nightly dumps mean you can lose up to a day; managed plans offer far tighter recovery windows and that difference is real. - Support. When something breaks at two in the morning, the person on call is you. ### README must contain - The server specification and why 4 GB is a floor rather than a target - That every shipped example secret was replaced, and where the real ones are stored - That Studio is not publicly reachable and exactly how to reach it - The backup schedule, both artefacts, the destination and the retention - The date and duration of the last restore drill, and the upgrade procedure ===== AGENTS.md ===== # Agent instructions · Supabase Pro indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: The official supabase/docker Compose stack, One Linux server with at least 4 GB of memory and 2 virtual CPUs, Docker Engine 20.10 or newer with Compose v2, Kong, which the stack already includes, listening on port 8000, Caddy in front of Kong for certificates and hostnames, pg_dump plus a copy of the storage volume, sent off the machine. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Docker publishes ports by writing iptables rules that bypass the usual firewall front end, so a service you thought was blocked can be wide open. Verify with a port scan from a different machine rather than by reading your firewall configuration. - The single most common self-hosting mistake is leaving the example values in place. The shipped anon key, service role key, JWT secret and dashboard password are published in the repository, so a stack running with them is readable and writable by anyone who finds it. Generate all of them before the first time the port is reachable, not after. - Studio talks to the stack with the service role key, which bypasses every row level security policy you will ever write. Exposing it to the internet behind only a password makes that password equivalent to the whole database. - A database dump does not contain uploaded files, and a storage archive does not contain the database. Backing up only one gives you a restore that produces rows pointing at files that are gone, or files nobody can find. Back up both, on the same schedule, and restore both in the drill. - Restoring onto the same server proves almost nothing, because the parts you forgot are still sitting there. The drill has value precisely because the second machine has none of your undocumented state. ===== BUILD_PLAN.md ===== # Build plan · Supabase Pro The official Supabase stack running on one server behind TLS, with generated secrets rather than the shipped placeholders, Studio locked away from the public internet, nightly database and storage backups going off the machine, and a restore you have performed on a second server with the date recorded. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Server, Docker and a closed front door A server exists, runs containers, and exposes nothing to the internet that you have not decided to expose. ### Steps 1. Provision and harden the server Create a non-root user with sudo, disable password authentication for SSH, and enable unattended security updates. ```sh adduser deploy && usermod -aG sudo deploy systemctl status ssh ``` 2. Install Docker Engine and Compose v2 Use Docker's own repository rather than a distribution package, which is often several versions behind. ```sh docker --version docker compose version ``` 3. Close the firewall and verify from outside Allow SSH, 80 and 443 only. Then scan from your laptop. Trusting the configuration file is how people expose databases. ```sh ufw allow OpenSSH && ufw allow 80,443/tcp && ufw enable nmap -Pn YOUR_SERVER_IP ``` ### Done when - [ ] docker compose version reports v2 or higher - [ ] A scan from another machine shows only the ports you intended - [ ] Port 5432 is unreachable from outside the server - [ ] SSH password authentication is disabled ### Watch out - Docker publishes ports by writing iptables rules that bypass the usual firewall front end, so a service you thought was blocked can be wide open. Verify with a port scan from a different machine rather than by reading your firewall configuration. ## Phase 2 · The stack up, with secrets that are yours Supabase runs, and not one credential is the value that shipped in the example file. ### Steps 1. Clone the stack and prepare the environment file Take only the docker directory. Copy .env.example to .env, then treat every line of it as something you must replace. Files: `docker/.env` ```sh git clone --depth 1 https://github.com/supabase/supabase cp supabase/docker/.env.example supabase/docker/.env ``` 2. Generate every secret A long random PostgreSQL password, a long random JWT secret, then derive the anon and service role keys by signing the documented payloads with that secret. Store them in a password manager as you go. ```sh openssl rand -base64 48 ``` 3. Bring it up and check health Start the stack and read the health of every container. A container in a restart loop is the normal first outcome and the logs say why. ```sh docker compose up -d docker compose ps ``` ### Done when - [ ] Every container reports healthy rather than restarting - [ ] No published example secret appears anywhere in the environment file - [ ] A request using the example anon key is rejected - [ ] The stack survives a full server reboot and comes back healthy ### Watch out - The single most common self-hosting mistake is leaving the example values in place. The shipped anon key, service role key, JWT secret and dashboard password are published in the repository, so a stack running with them is readable and writable by anyone who finds it. Generate all of them before the first time the port is reachable, not after. ## Phase 3 · TLS, hostnames and a private Studio The API is reachable over HTTPS at your domain, and the dashboard is not reachable by the public at all. ### Steps 1. Point DNS and run Caddy in front of Kong An A record for the API hostname. Caddy proxies to Kong on 8000 and obtains certificates on first request. Files: `Caddyfile` 2. Keep Studio off the public internet Bind its published port to 127.0.0.1 in the compose override, then reach it with ssh -L when you need it. Files: `docker-compose.override.yml` ```sh ssh -L 3000:localhost:3000 deploy@YOUR_SERVER ``` 3. Verify from outside, not from the server Curl the API host from your laptop, and try the Studio port from your laptop too. The second one must fail. ```sh curl -I https://api.example.com/rest/v1/ ``` ### Done when - [ ] The API answers over HTTPS with a valid certificate - [ ] The Studio port is refused from another machine - [ ] Studio works through an SSH tunnel and can list tables - [ ] HTTP redirects to HTTPS ### Watch out - Studio talks to the stack with the service role key, which bypasses every row level security policy you will ever write. Exposing it to the internet behind only a password makes that password equivalent to the whole database. ## Phase 4 · Backups of both things that matter The database and the uploaded files both leave the server on a schedule. ### Steps 1. Dump the database on a schedule pg_dump from a container or from the host, gzipped and dated, run by cron or a systemd timer. ```sh docker compose exec -T db pg_dump -U postgres postgres | gzip > db-$(date +%F).sql.gz ``` 2. Archive the storage volume Tar the storage volume to the same destination with the same date stamp so the two halves of a restore are obviously paired. ```sh tar czf storage-$(date +%F).tar.gz -C ./volumes storage ``` 3. Ship it off the machine and alert on absence Copy to object storage or another host. Then check each morning that a file with today's date exists, and alert when it does not. Files: `scripts/backup.sh`, `scripts/check_backup.sh` ### Done when - [ ] A dated database dump and a dated storage archive both land off the server nightly - [ ] The dump is a valid gzip and its size is plausible for the data volume - [ ] A missing backup produces an alert - [ ] Several generations are retained rather than one file being overwritten ### Watch out - A database dump does not contain uploaded files, and a storage archive does not contain the database. Backing up only one gives you a restore that produces rows pointing at files that are gone, or files nobody can find. Back up both, on the same schedule, and restore both in the drill. ## Phase 5 · The restore drill, actually performed You have brought the whole thing back on a different server, and you know how long it took. ### Steps 1. Stand up a clean second server Same Docker setup, fresh clone of the stack, and the same environment file, which means the secrets are part of your backup and not only in your head. 2. Restore both halves Load the dump into the fresh database and unpack the storage archive into the storage volume before starting the storage service. ```sh gunzip -c db-2026-09-07.sql.gz | docker compose exec -T db psql -U postgres postgres ``` 3. Verify and write it down Compare row counts, download a file that existed before the backup, and sign in with an existing account. Then record the date and how long it took. Files: `README.md` ### Done when - [ ] Row counts on the restored server match the source - [ ] A file that existed before the backup downloads from the restored stack - [ ] An existing user can authenticate against the restored stack - [ ] The README records the date and duration of this drill ### Watch out - Restoring onto the same server proves almost nothing, because the parts you forgot are still sitting there. The drill has value precisely because the second machine has none of your undocumented state. ## Not in this build - A global edge network and the latency that comes with one. - Managed database operations, patching, tuning and failover. - Autoscaling, read replicas and connection pooling at scale. - Point in time recovery. Nightly dumps mean a bad day can cost you a day. - Anyone to call when it breaks. ## After v1, if you want it - Point in time recovery with continuous archiving, once nightly dumps stop feeling sufficient - A staging stack restored from last night's backup, which also exercises the restore weekly - Read replicas, but only after measuring that one server is genuinely the limit ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Generate with openssl rand. Never the shipped example. POSTGRES_PASSWORD=generate-a-long-random-value # Required · secret. Generate it, then derive the anon and service role keys from it. It is part of a working restore, so back it up. JWT_SECRET=generate-a-long-random-value # Required. Signed with your JWT secret. Public by design, but it must be yours rather than the published example. ANON_KEY=eyJhbGciOi... # Required · secret. Signed with your JWT secret. Bypasses row level security entirely, so it never reaches a browser. SERVICE_ROLE_KEY=eyJhbGciOi... # Required · secret. Studio's own login. Change it even though Studio is not publicly exposed. DASHBOARD_PASSWORD=generate-a-long-random-value # Required. Where auth redirects send users after sign-in. SITE_URL=https://app.example.com # Required. The off-server destination for both nightly artefacts. BACKUP_DEST=s3://your-bucket/supabase
You are building a production product version of Supabase Pro. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Supabase Pro · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Supabase Pro, self-host a Postgres backend with auth, storage, and a small API surface. The hard boundary is managed postgres, backups, auth delivery, storage, edge functions, observability, and support, plus infrastructure scale, operations, and reliability. ## Product outcome A Supabase stack you own end to end, with no default credential anywhere, a dashboard that is not exposed, both halves of the data backed up nightly off the machine, and a restore you have performed on a different server and dated. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Linux server - Docker - domain and DNS access - backup destination - basic systems administration ## Explicit non-goals for v1 - A global edge network and the latency that comes with one. - Managed database operations, patching, tuning and failover. - Autoscaling, read replicas and connection pooling at scale. - Point in time recovery. Nightly dumps mean a bad day can cost you a day. - Anyone to call when it breaks. - managed Postgres, backups, auth delivery, storage, edge functions, observability, and support - global edge network - managed databases - autoscaling - DDoS protection, support, and compliance ## Success criteria - No published example secret remains anywhere in the configuration - Studio is confirmed unreachable from another machine - Both backup artefacts exist off the server and are dated - A restore has been completed on a second server and the date recorded - Alerts have been tested by causing the condition, not by reading the configuration ===== BRIEF.md ===== # Build brief · Supabase Pro The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a self-hosted Supabase stack on one server you own, with backups you have actually restored to replace Supabase Pro. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Do not write a backend as a service. Supabase publishes its own Docker Compose stack and it is the thing to deploy. This is an operations task, not a build task: every phase below is deploy, configure and verify. If you find yourself writing an auth server or a storage API, stop and go back to the compose file. The last phase is a restore drill and it must actually be performed, with the date written down. A backup you have never restored is a belief, not a backup. ### Stack (fixed, do not substitute) - Platform: The official supabase/docker Compose stack · thirteen services that already work together; assembling them yourself is a month and a worse result - Host: One Linux server with at least 4 GB of memory and 2 virtual CPUs · the documented floor; 8 GB is the realistic figure once you have data and any traffic - Container runtime: Docker Engine 20.10 or newer with Compose v2 · what the stack's compose file expects - Gateway: Kong, which the stack already includes, listening on port 8000 · one entry point in front of PostgREST, auth, storage and realtime - Edge: Caddy in front of Kong for certificates and hostnames · automatic TLS, and a place to refuse traffic to Studio - Backups: pg_dump plus a copy of the storage volume, sent off the machine · the database and the uploaded files are two separate things and a dump contains only one of them ### Data model (create this before Phase 1) - You are not designing a schema here. What you are designing is the operational state, and it is worth writing down before you start. - secrets · POSTGRES_PASSWORD, JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY, DASHBOARD_PASSWORD, all generated by you and none of them the shipped example values - volumes · the PostgreSQL data directory and the storage volume, which are the two things whose loss is unrecoverable - backups · dated database dumps and dated storage archives, in a destination that is not this server - restore log · the date of every restore drill you have performed, kept in the README ### Phase 1 · Server, Docker and a closed front door Build: Provision a server with at least 4 GB of memory. Install Docker Engine and the compose plugin. Create a non-root user. Configure the firewall to allow only SSH and the web ports, and confirm from another machine that the database port is not reachable. Trap: Docker publishes ports by writing iptables rules that bypass the usual firewall front end, so a service you thought was blocked can be wide open. Verify with a port scan from a different machine rather than by reading your firewall configuration. Done when: Docker runs a test container as the non-root user. A port scan from another machine shows only SSH and the web ports open. Port 5432 is not reachable from outside the server. Do not build yet: No Supabase yet. A correctly closed server first. ### Phase 2 · The stack up, with secrets that are yours Build: Clone the supabase/docker directory, copy the example environment file, and replace every secret with a generated value. Generate the anon and service role keys from your own JWT secret. Bring the stack up and confirm every service reports healthy. Trap: The single most common self-hosting mistake is leaving the example values in place. The shipped anon key, service role key, JWT secret and dashboard password are published in the repository, so a stack running with them is readable and writable by anyone who finds it. Generate all of them before the first time the port is reachable, not after. Done when: Every container reports healthy. Grepping the environment file for the published example values returns nothing. A request signed with the example anon key is rejected. The service role key is not present in any file outside the environment file and your secret store. Do not build yet: No TLS and no public hostname yet. Keep it bound to localhost until the next phase. ### Phase 3 · TLS, hostnames and a private Studio Build: Point a domain at the server. Put Caddy in front of Kong so certificates are automatic. Expose the API host publicly. Do not expose Studio: bind it to localhost and reach it through an SSH tunnel, or put it behind an allowlist and its own authentication. Trap: Studio talks to the stack with the service role key, which bypasses every row level security policy you will ever write. Exposing it to the internet behind only a password makes that password equivalent to the whole database. Done when: The API answers over HTTPS with a valid certificate. Studio is not reachable from another machine, verified by trying. Studio is reachable through the tunnel and can list tables. A plain HTTP request is redirected to HTTPS. Do not build yet: No backups yet, which is the next phase and the important one. ### Phase 4 · Backups of both things that matter Build: A nightly pg_dump of the database and a nightly archive of the storage volume, both sent to a destination that is not this server, both dated, with several generations kept. Alert if a night passes with no new backup. Trap: A database dump does not contain uploaded files, and a storage archive does not contain the database. Backing up only one gives you a restore that produces rows pointing at files that are gone, or files nobody can find. Back up both, on the same schedule, and restore both in the drill. Done when: Two dated artefacts appear off the server each night. The database dump can be listed and its size is plausible. Deleting the newest dump and waiting a night produces a replacement and no alert; suppressing the job for a night produces an alert. Do not build yet: Do not call this done until Phase 5 has actually restored these artefacts. ### Phase 5 · The restore drill, actually performed Build: Provision a second server. Bring up a fresh stack. Load the newest database dump and unpack the newest storage archive. Verify row counts and that a stored file downloads. Write the date, the duration and anything that surprised you in the README. Trap: Restoring onto the same server proves almost nothing, because the parts you forgot are still sitting there. The drill has value precisely because the second machine has none of your undocumented state. Done when: A second server serves the same data. Row counts for the largest tables match the source. A file uploaded before the backup downloads correctly from the restored stack. Authentication works, meaning the JWT secret was part of what you restored. The date and duration are in the README. Do not build yet: Do not treat monitoring as a substitute for having done this. ### Phase 6 · Monitoring, upgrades and the incident you will have Build: Uptime checks against the API and the database. Disk alerts, because a full disk is the most common way this stack stops. A documented upgrade procedure that takes a backup first. Log retention that does not itself fill the disk. Trap: The stack's containers are versioned together and upgrading one service in isolation is how you get a database that the rest of the stack cannot talk to. Upgrade by moving the whole compose file forward, after a backup, and read the release notes for migration steps. Done when: An uptime check alerts within minutes when the API stops answering, tested by stopping it. A disk alert fires at a threshold that leaves room to act, tested by filling a scratch file. An upgrade has been performed on the second server first and only then on the live one. Do not build yet: Nothing after this. ### Out of scope (and why) - The global edge network. Your stack is on one machine in one place, and a visitor on another continent will feel that. - Managed database operations. Patching, tuning, connection limits and failover are now yours, and they are the largest part of what the subscription buys. - Autoscaling and read replicas. One server does not scale sideways, and pretending otherwise is how outages happen. - Point in time recovery. Nightly dumps mean you can lose up to a day; managed plans offer far tighter recovery windows and that difference is real. - Support. When something breaks at two in the morning, the person on call is you. ### README must contain - The server specification and why 4 GB is a floor rather than a target - That every shipped example secret was replaced, and where the real ones are stored - That Studio is not publicly reachable and exactly how to reach it - The backup schedule, both artefacts, the destination and the retention - The date and duration of the last restore drill, and the upgrade procedure ===== ARCHITECTURE.md ===== # Architecture · Supabase Pro ## Stack | Part | Choice | Why | | --- | --- | --- | | Platform | The official supabase/docker Compose stack | thirteen services that already work together; assembling them yourself is a month and a worse result | | Host | One Linux server with at least 4 GB of memory and 2 virtual CPUs | the documented floor; 8 GB is the realistic figure once you have data and any traffic | | Container runtime | Docker Engine 20.10 or newer with Compose v2 | what the stack's compose file expects | | Gateway | Kong, which the stack already includes, listening on port 8000 | one entry point in front of PostgREST, auth, storage and realtime | | Edge | Caddy in front of Kong for certificates and hostnames | automatic TLS, and a place to refuse traffic to Studio | | Backups | pg_dump plus a copy of the storage volume, sent off the machine | the database and the uploaded files are two separate things and a dump contains only one of them | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Compose stack | Every Supabase service and how they talk to each other. | Upgrade by moving the whole compose file forward together. Individual service versions are not independently swappable and treating them as such breaks the stack. | | Edge | TLS, hostnames and what is publicly reachable. | Caddy, nginx or Traefik are interchangeable. What must not change is that Studio stays private. | | Secrets | The five generated credentials. | Rotating the JWT secret invalidates existing tokens and is a planned event, not a maintenance task. | | Backups | Nightly database and storage artefacts sent off the machine. | The destination is replaceable. The rule that both artefacts are taken together is not. | | Monitoring | Knowing before your users do. | Any uptime service. The checks that matter are the API, a real database query, and free disk. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `POSTGRES_PASSWORD` · required, secret · Generate with openssl rand. Never the shipped example. - `JWT_SECRET` · required, secret · Generate it, then derive the anon and service role keys from it. It is part of a working restore, so back it up. - `ANON_KEY` · required · Signed with your JWT secret. Public by design, but it must be yours rather than the published example. - `SERVICE_ROLE_KEY` · required, secret · Signed with your JWT secret. Bypasses row level security entirely, so it never reaches a browser. - `DASHBOARD_PASSWORD` · required, secret · Studio's own login. Change it even though Studio is not publicly exposed. - `SITE_URL` · required · Where auth redirects send users after sign-in. - `BACKUP_DEST` · required · The off-server destination for both nightly artefacts. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Supabase Pro product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: The official supabase/docker Compose stack, One Linux server with at least 4 GB of memory and 2 virtual CPUs, Docker Engine 20.10 or newer with Compose v2, Kong, which the stack already includes, listening on port 8000, Caddy in front of Kong for certificates and hostnames, pg_dump plus a copy of the storage volume, sent off the machine. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Docker publishes ports by writing iptables rules that bypass the usual firewall front end, so a service you thought was blocked can be wide open. Verify with a port scan from a different machine rather than by reading your firewall configuration. - The single most common self-hosting mistake is leaving the example values in place. The shipped anon key, service role key, JWT secret and dashboard password are published in the repository, so a stack running with them is readable and writable by anyone who finds it. Generate all of them before the first time the port is reachable, not after. - Studio talks to the stack with the service role key, which bypasses every row level security policy you will ever write. Exposing it to the internet behind only a password makes that password equivalent to the whole database. - A database dump does not contain uploaded files, and a storage archive does not contain the database. Backing up only one gives you a restore that produces rows pointing at files that are gone, or files nobody can find. Back up both, on the same schedule, and restore both in the drill. - Restoring onto the same server proves almost nothing, because the parts you forgot are still sitting there. The drill has value precisely because the second machine has none of your undocumented state. - The stack's containers are versioned together and upgrading one service in isolation is how you get a database that the rest of the stack cannot talk to. Upgrade by moving the whole compose file forward, after a backup, and read the release notes for migration steps. ===== MILESTONES.md ===== # Delivery milestones · Supabase Pro Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Server, Docker and a closed front door A server exists, runs containers, and exposes nothing to the internet that you have not decided to expose. ### Steps 1. Provision and harden the server Create a non-root user with sudo, disable password authentication for SSH, and enable unattended security updates. ```sh adduser deploy && usermod -aG sudo deploy systemctl status ssh ``` 2. Install Docker Engine and Compose v2 Use Docker's own repository rather than a distribution package, which is often several versions behind. ```sh docker --version docker compose version ``` 3. Close the firewall and verify from outside Allow SSH, 80 and 443 only. Then scan from your laptop. Trusting the configuration file is how people expose databases. ```sh ufw allow OpenSSH && ufw allow 80,443/tcp && ufw enable nmap -Pn YOUR_SERVER_IP ``` ### Done when - [ ] docker compose version reports v2 or higher - [ ] A scan from another machine shows only the ports you intended - [ ] Port 5432 is unreachable from outside the server - [ ] SSH password authentication is disabled ### Watch out - Docker publishes ports by writing iptables rules that bypass the usual firewall front end, so a service you thought was blocked can be wide open. Verify with a port scan from a different machine rather than by reading your firewall configuration. ## M2 · The stack up, with secrets that are yours Supabase runs, and not one credential is the value that shipped in the example file. ### Steps 1. Clone the stack and prepare the environment file Take only the docker directory. Copy .env.example to .env, then treat every line of it as something you must replace. Files: `docker/.env` ```sh git clone --depth 1 https://github.com/supabase/supabase cp supabase/docker/.env.example supabase/docker/.env ``` 2. Generate every secret A long random PostgreSQL password, a long random JWT secret, then derive the anon and service role keys by signing the documented payloads with that secret. Store them in a password manager as you go. ```sh openssl rand -base64 48 ``` 3. Bring it up and check health Start the stack and read the health of every container. A container in a restart loop is the normal first outcome and the logs say why. ```sh docker compose up -d docker compose ps ``` ### Done when - [ ] Every container reports healthy rather than restarting - [ ] No published example secret appears anywhere in the environment file - [ ] A request using the example anon key is rejected - [ ] The stack survives a full server reboot and comes back healthy ### Watch out - The single most common self-hosting mistake is leaving the example values in place. The shipped anon key, service role key, JWT secret and dashboard password are published in the repository, so a stack running with them is readable and writable by anyone who finds it. Generate all of them before the first time the port is reachable, not after. ## M3 · TLS, hostnames and a private Studio The API is reachable over HTTPS at your domain, and the dashboard is not reachable by the public at all. ### Steps 1. Point DNS and run Caddy in front of Kong An A record for the API hostname. Caddy proxies to Kong on 8000 and obtains certificates on first request. Files: `Caddyfile` 2. Keep Studio off the public internet Bind its published port to 127.0.0.1 in the compose override, then reach it with ssh -L when you need it. Files: `docker-compose.override.yml` ```sh ssh -L 3000:localhost:3000 deploy@YOUR_SERVER ``` 3. Verify from outside, not from the server Curl the API host from your laptop, and try the Studio port from your laptop too. The second one must fail. ```sh curl -I https://api.example.com/rest/v1/ ``` ### Done when - [ ] The API answers over HTTPS with a valid certificate - [ ] The Studio port is refused from another machine - [ ] Studio works through an SSH tunnel and can list tables - [ ] HTTP redirects to HTTPS ### Watch out - Studio talks to the stack with the service role key, which bypasses every row level security policy you will ever write. Exposing it to the internet behind only a password makes that password equivalent to the whole database. ## M4 · Backups of both things that matter The database and the uploaded files both leave the server on a schedule. ### Steps 1. Dump the database on a schedule pg_dump from a container or from the host, gzipped and dated, run by cron or a systemd timer. ```sh docker compose exec -T db pg_dump -U postgres postgres | gzip > db-$(date +%F).sql.gz ``` 2. Archive the storage volume Tar the storage volume to the same destination with the same date stamp so the two halves of a restore are obviously paired. ```sh tar czf storage-$(date +%F).tar.gz -C ./volumes storage ``` 3. Ship it off the machine and alert on absence Copy to object storage or another host. Then check each morning that a file with today's date exists, and alert when it does not. Files: `scripts/backup.sh`, `scripts/check_backup.sh` ### Done when - [ ] A dated database dump and a dated storage archive both land off the server nightly - [ ] The dump is a valid gzip and its size is plausible for the data volume - [ ] A missing backup produces an alert - [ ] Several generations are retained rather than one file being overwritten ### Watch out - A database dump does not contain uploaded files, and a storage archive does not contain the database. Backing up only one gives you a restore that produces rows pointing at files that are gone, or files nobody can find. Back up both, on the same schedule, and restore both in the drill. ## M5 · The restore drill, actually performed You have brought the whole thing back on a different server, and you know how long it took. ### Steps 1. Stand up a clean second server Same Docker setup, fresh clone of the stack, and the same environment file, which means the secrets are part of your backup and not only in your head. 2. Restore both halves Load the dump into the fresh database and unpack the storage archive into the storage volume before starting the storage service. ```sh gunzip -c db-2026-09-07.sql.gz | docker compose exec -T db psql -U postgres postgres ``` 3. Verify and write it down Compare row counts, download a file that existed before the backup, and sign in with an existing account. Then record the date and how long it took. Files: `README.md` ### Done when - [ ] Row counts on the restored server match the source - [ ] A file that existed before the backup downloads from the restored stack - [ ] An existing user can authenticate against the restored stack - [ ] The README records the date and duration of this drill ### Watch out - Restoring onto the same server proves almost nothing, because the parts you forgot are still sitting there. The drill has value precisely because the second machine has none of your undocumented state. ## M6 · Monitoring, upgrades and the incident you will have (production only) You find out about problems before your users do, and an upgrade is a planned event. ### Steps 1. Monitor the API, the database and the disk An external uptime check for the API, a query check for the database, and a disk threshold at around seventy five percent so there is time to react. 2. Write the upgrade procedure Back up, pull the new compose file, read the release notes, apply on the second server, verify, then apply on the live one. Written down, because you will do this while distracted. Files: `docs/upgrade.md` 3. Cap log growth Set the Docker logging driver's max size and file count, or the container logs will quietly consume the disk over months. Files: `docker-compose.override.yml` ### Done when - [ ] Stopping the API triggers an alert within minutes - [ ] A disk threshold alert fires when tested with a scratch file - [ ] Container logs are capped and cannot grow without bound - [ ] An upgrade was rehearsed on the second server before the live one ### Watch out - The stack's containers are versioned together and upgrading one service in isolation is how you get a database that the rest of the stack cannot talk to. Upgrade by moving the whole compose file forward, after a backup, and read the release notes for migration steps. ===== OPERATIONS.md ===== # Operations · Supabase Pro ## Backup Nightly pg_dump and a nightly storage volume archive, dated, several generations, off the server. The environment file holding the JWT secret belongs in your secret store, because a restore without it cannot authenticate existing users. ## Restore Fresh server, fresh stack, same environment file, load the dump, unpack the storage archive, verify row counts and a file download, then sign in as an existing user. Perform this on a schedule and record each date. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring External uptime on the API, a query-level database check, disk usage with an alert at around seventy five percent, and a morning check that last night's backup exists. Disk is the one that actually bites. ## Incident checklist For a failed upgrade, restore the pre-upgrade dump onto a clean stack rather than trying to repair in place. For a disk full event, the storage volume and the container logs are the usual causes, in that order. For a leaked service role key, rotate the JWT secret and accept that every existing session ends. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] No published example secret remains anywhere in the configuration - [ ] Studio is confirmed unreachable from another machine - [ ] Both backup artefacts exist off the server and are dated - [ ] A restore has been completed on a second server and the date recorded - [ ] Alerts have been tested by causing the condition, not by reading the configuration ## Launch constraint Do not market omitted Supabase Pro capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Generate with openssl rand. Never the shipped example. POSTGRES_PASSWORD=generate-a-long-random-value # Required · secret. Generate it, then derive the anon and service role keys from it. It is part of a working restore, so back it up. JWT_SECRET=generate-a-long-random-value # Required. Signed with your JWT secret. Public by design, but it must be yours rather than the published example. ANON_KEY=eyJhbGciOi... # Required · secret. Signed with your JWT secret. Bypasses row level security entirely, so it never reaches a browser. SERVICE_ROLE_KEY=eyJhbGciOi... # Required · secret. Studio's own login. Change it even though Studio is not publicly exposed. DASHBOARD_PASSWORD=generate-a-long-random-value # Required. Where auth redirects send users after sign-in. SITE_URL=https://app.example.com # Required. The off-server destination for both nightly artefacts. BACKUP_DEST=s3://your-bucket/supabase
# Supabase Pro · indie build The official Supabase stack running on one server behind TLS, with generated secrets rather than the shipped placeholders, Studio locked away from the public internet, nightly database and storage backups going off the machine, and a restore you have performed on a second server with the date recorded. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Platform | The official supabase/docker Compose stack | thirteen services that already work together; assembling them yourself is a month and a worse result | | Host | One Linux server with at least 4 GB of memory and 2 virtual CPUs | the documented floor; 8 GB is the realistic figure once you have data and any traffic | | Container runtime | Docker Engine 20.10 or newer with Compose v2 | what the stack's compose file expects | | Gateway | Kong, which the stack already includes, listening on port 8000 | one entry point in front of PostgREST, auth, storage and realtime | | Edge | Caddy in front of Kong for certificates and hostnames | automatic TLS, and a place to refuse traffic to Studio | | Backups | pg_dump plus a copy of the storage volume, sent off the machine | the database and the uploaded files are two separate things and a dump contains only one of them | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A Linux server with 4 GB of memory and 2 virtual CPUs** · roughly 20 to 40 USD a month at 8 GB with most providers - Why: Below this the stack starts and then dies under any real use. 8 GB is the honest recommendation once you have data. - Get it: Any virtual server provider. Choose a region near your users and a plan you can resize later. - Verify: free -m reports 4 GB or more and docker runs - [ ] **A second server for the restore drill** · a few USD if you delete it the same day - Why: The drill is worthless on the machine that already has your state. You can destroy this one afterwards. - Get it: The same provider, the smallest plan that meets the memory floor. Create it for the drill and delete it after. - Verify: You can SSH to it and run Docker - [ ] **A domain name with DNS you control** · roughly 10 to 15 USD a year - Why: Certificates need a hostname, and the API needs a stable address. - Get it: Any registrar. Add an A record for the API hostname pointing at the server. - Verify: dig your hostname returns the server address - [ ] **An off-server backup destination** · a few USD a month at these volumes; the first gigabytes are often free - Why: Backups on the server die with the server, which is the exact scenario you are protecting against. - Get it: Object storage from any provider, or a second host you control. Create a bucket and credentials scoped to write only into it. - Verify: A test file uploads and can be listed from your laptop - [ ] **A place to keep the generated secrets** · free - Why: The JWT secret is part of a working restore. Losing it means existing tokens and accounts do not survive. - Get it: A password manager entry, or an encrypted file kept somewhere other than the server. Record the anon key, service role key, JWT secret, database password and dashboard password. - Verify: You can retrieve all five from somewhere that is not the server ## Quick start ```sh adduser deploy && usermod -aG sudo deploy systemctl status ssh ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - A global edge network and the latency that comes with one. - Managed database operations, patching, tuning and failover. - Autoscaling, read replicas and connection pooling at scale. - Point in time recovery. Nightly dumps mean a bad day can cost you a day. - Anyone to call when it breaks. - managed Postgres, backups, auth delivery, storage, edge functions, observability, and support - global edge network - managed databases - autoscaling - DDoS protection, support, and compliance If one of those is essential to you, that is the reason to keep paying for Supabase Pro, and the README should say so rather than pretend.
# Build brief · Supabase Pro The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a self-hosted Supabase stack on one server you own, with backups you have actually restored to replace Supabase Pro. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Do not write a backend as a service. Supabase publishes its own Docker Compose stack and it is the thing to deploy. This is an operations task, not a build task: every phase below is deploy, configure and verify. If you find yourself writing an auth server or a storage API, stop and go back to the compose file. The last phase is a restore drill and it must actually be performed, with the date written down. A backup you have never restored is a belief, not a backup. ### Stack (fixed, do not substitute) - Platform: The official supabase/docker Compose stack · thirteen services that already work together; assembling them yourself is a month and a worse result - Host: One Linux server with at least 4 GB of memory and 2 virtual CPUs · the documented floor; 8 GB is the realistic figure once you have data and any traffic - Container runtime: Docker Engine 20.10 or newer with Compose v2 · what the stack's compose file expects - Gateway: Kong, which the stack already includes, listening on port 8000 · one entry point in front of PostgREST, auth, storage and realtime - Edge: Caddy in front of Kong for certificates and hostnames · automatic TLS, and a place to refuse traffic to Studio - Backups: pg_dump plus a copy of the storage volume, sent off the machine · the database and the uploaded files are two separate things and a dump contains only one of them ### Data model (create this before Phase 1) - You are not designing a schema here. What you are designing is the operational state, and it is worth writing down before you start. - secrets · POSTGRES_PASSWORD, JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY, DASHBOARD_PASSWORD, all generated by you and none of them the shipped example values - volumes · the PostgreSQL data directory and the storage volume, which are the two things whose loss is unrecoverable - backups · dated database dumps and dated storage archives, in a destination that is not this server - restore log · the date of every restore drill you have performed, kept in the README ### Phase 1 · Server, Docker and a closed front door Build: Provision a server with at least 4 GB of memory. Install Docker Engine and the compose plugin. Create a non-root user. Configure the firewall to allow only SSH and the web ports, and confirm from another machine that the database port is not reachable. Trap: Docker publishes ports by writing iptables rules that bypass the usual firewall front end, so a service you thought was blocked can be wide open. Verify with a port scan from a different machine rather than by reading your firewall configuration. Done when: Docker runs a test container as the non-root user. A port scan from another machine shows only SSH and the web ports open. Port 5432 is not reachable from outside the server. Do not build yet: No Supabase yet. A correctly closed server first. ### Phase 2 · The stack up, with secrets that are yours Build: Clone the supabase/docker directory, copy the example environment file, and replace every secret with a generated value. Generate the anon and service role keys from your own JWT secret. Bring the stack up and confirm every service reports healthy. Trap: The single most common self-hosting mistake is leaving the example values in place. The shipped anon key, service role key, JWT secret and dashboard password are published in the repository, so a stack running with them is readable and writable by anyone who finds it. Generate all of them before the first time the port is reachable, not after. Done when: Every container reports healthy. Grepping the environment file for the published example values returns nothing. A request signed with the example anon key is rejected. The service role key is not present in any file outside the environment file and your secret store. Do not build yet: No TLS and no public hostname yet. Keep it bound to localhost until the next phase. ### Phase 3 · TLS, hostnames and a private Studio Build: Point a domain at the server. Put Caddy in front of Kong so certificates are automatic. Expose the API host publicly. Do not expose Studio: bind it to localhost and reach it through an SSH tunnel, or put it behind an allowlist and its own authentication. Trap: Studio talks to the stack with the service role key, which bypasses every row level security policy you will ever write. Exposing it to the internet behind only a password makes that password equivalent to the whole database. Done when: The API answers over HTTPS with a valid certificate. Studio is not reachable from another machine, verified by trying. Studio is reachable through the tunnel and can list tables. A plain HTTP request is redirected to HTTPS. Do not build yet: No backups yet, which is the next phase and the important one. ### Phase 4 · Backups of both things that matter Build: A nightly pg_dump of the database and a nightly archive of the storage volume, both sent to a destination that is not this server, both dated, with several generations kept. Alert if a night passes with no new backup. Trap: A database dump does not contain uploaded files, and a storage archive does not contain the database. Backing up only one gives you a restore that produces rows pointing at files that are gone, or files nobody can find. Back up both, on the same schedule, and restore both in the drill. Done when: Two dated artefacts appear off the server each night. The database dump can be listed and its size is plausible. Deleting the newest dump and waiting a night produces a replacement and no alert; suppressing the job for a night produces an alert. Do not build yet: Do not call this done until Phase 5 has actually restored these artefacts. ### Phase 5 · The restore drill, actually performed Build: Provision a second server. Bring up a fresh stack. Load the newest database dump and unpack the newest storage archive. Verify row counts and that a stored file downloads. Write the date, the duration and anything that surprised you in the README. Trap: Restoring onto the same server proves almost nothing, because the parts you forgot are still sitting there. The drill has value precisely because the second machine has none of your undocumented state. Done when: A second server serves the same data. Row counts for the largest tables match the source. A file uploaded before the backup downloads correctly from the restored stack. Authentication works, meaning the JWT secret was part of what you restored. The date and duration are in the README. Do not build yet: Do not treat monitoring as a substitute for having done this. ### Phase 6 · Monitoring, upgrades and the incident you will have Build: Uptime checks against the API and the database. Disk alerts, because a full disk is the most common way this stack stops. A documented upgrade procedure that takes a backup first. Log retention that does not itself fill the disk. Trap: The stack's containers are versioned together and upgrading one service in isolation is how you get a database that the rest of the stack cannot talk to. Upgrade by moving the whole compose file forward, after a backup, and read the release notes for migration steps. Done when: An uptime check alerts within minutes when the API stops answering, tested by stopping it. A disk alert fires at a threshold that leaves room to act, tested by filling a scratch file. An upgrade has been performed on the second server first and only then on the live one. Do not build yet: Nothing after this. ### Out of scope (and why) - The global edge network. Your stack is on one machine in one place, and a visitor on another continent will feel that. - Managed database operations. Patching, tuning, connection limits and failover are now yours, and they are the largest part of what the subscription buys. - Autoscaling and read replicas. One server does not scale sideways, and pretending otherwise is how outages happen. - Point in time recovery. Nightly dumps mean you can lose up to a day; managed plans offer far tighter recovery windows and that difference is real. - Support. When something breaks at two in the morning, the person on call is you. ### README must contain - The server specification and why 4 GB is a floor rather than a target - That every shipped example secret was replaced, and where the real ones are stored - That Studio is not publicly reachable and exactly how to reach it - The backup schedule, both artefacts, the destination and the retention - The date and duration of the last restore drill, and the upgrade procedure
# Agent instructions · Supabase Pro indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: The official supabase/docker Compose stack, One Linux server with at least 4 GB of memory and 2 virtual CPUs, Docker Engine 20.10 or newer with Compose v2, Kong, which the stack already includes, listening on port 8000, Caddy in front of Kong for certificates and hostnames, pg_dump plus a copy of the storage volume, sent off the machine. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Docker publishes ports by writing iptables rules that bypass the usual firewall front end, so a service you thought was blocked can be wide open. Verify with a port scan from a different machine rather than by reading your firewall configuration. - The single most common self-hosting mistake is leaving the example values in place. The shipped anon key, service role key, JWT secret and dashboard password are published in the repository, so a stack running with them is readable and writable by anyone who finds it. Generate all of them before the first time the port is reachable, not after. - Studio talks to the stack with the service role key, which bypasses every row level security policy you will ever write. Exposing it to the internet behind only a password makes that password equivalent to the whole database. - A database dump does not contain uploaded files, and a storage archive does not contain the database. Backing up only one gives you a restore that produces rows pointing at files that are gone, or files nobody can find. Back up both, on the same schedule, and restore both in the drill. - Restoring onto the same server proves almost nothing, because the parts you forgot are still sitting there. The drill has value precisely because the second machine has none of your undocumented state.
# Build plan · Supabase Pro The official Supabase stack running on one server behind TLS, with generated secrets rather than the shipped placeholders, Studio locked away from the public internet, nightly database and storage backups going off the machine, and a restore you have performed on a second server with the date recorded. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Server, Docker and a closed front door A server exists, runs containers, and exposes nothing to the internet that you have not decided to expose. ### Steps 1. Provision and harden the server Create a non-root user with sudo, disable password authentication for SSH, and enable unattended security updates. ```sh adduser deploy && usermod -aG sudo deploy systemctl status ssh ``` 2. Install Docker Engine and Compose v2 Use Docker's own repository rather than a distribution package, which is often several versions behind. ```sh docker --version docker compose version ``` 3. Close the firewall and verify from outside Allow SSH, 80 and 443 only. Then scan from your laptop. Trusting the configuration file is how people expose databases. ```sh ufw allow OpenSSH && ufw allow 80,443/tcp && ufw enable nmap -Pn YOUR_SERVER_IP ``` ### Done when - [ ] docker compose version reports v2 or higher - [ ] A scan from another machine shows only the ports you intended - [ ] Port 5432 is unreachable from outside the server - [ ] SSH password authentication is disabled ### Watch out - Docker publishes ports by writing iptables rules that bypass the usual firewall front end, so a service you thought was blocked can be wide open. Verify with a port scan from a different machine rather than by reading your firewall configuration. ## Phase 2 · The stack up, with secrets that are yours Supabase runs, and not one credential is the value that shipped in the example file. ### Steps 1. Clone the stack and prepare the environment file Take only the docker directory. Copy .env.example to .env, then treat every line of it as something you must replace. Files: `docker/.env` ```sh git clone --depth 1 https://github.com/supabase/supabase cp supabase/docker/.env.example supabase/docker/.env ``` 2. Generate every secret A long random PostgreSQL password, a long random JWT secret, then derive the anon and service role keys by signing the documented payloads with that secret. Store them in a password manager as you go. ```sh openssl rand -base64 48 ``` 3. Bring it up and check health Start the stack and read the health of every container. A container in a restart loop is the normal first outcome and the logs say why. ```sh docker compose up -d docker compose ps ``` ### Done when - [ ] Every container reports healthy rather than restarting - [ ] No published example secret appears anywhere in the environment file - [ ] A request using the example anon key is rejected - [ ] The stack survives a full server reboot and comes back healthy ### Watch out - The single most common self-hosting mistake is leaving the example values in place. The shipped anon key, service role key, JWT secret and dashboard password are published in the repository, so a stack running with them is readable and writable by anyone who finds it. Generate all of them before the first time the port is reachable, not after. ## Phase 3 · TLS, hostnames and a private Studio The API is reachable over HTTPS at your domain, and the dashboard is not reachable by the public at all. ### Steps 1. Point DNS and run Caddy in front of Kong An A record for the API hostname. Caddy proxies to Kong on 8000 and obtains certificates on first request. Files: `Caddyfile` 2. Keep Studio off the public internet Bind its published port to 127.0.0.1 in the compose override, then reach it with ssh -L when you need it. Files: `docker-compose.override.yml` ```sh ssh -L 3000:localhost:3000 deploy@YOUR_SERVER ``` 3. Verify from outside, not from the server Curl the API host from your laptop, and try the Studio port from your laptop too. The second one must fail. ```sh curl -I https://api.example.com/rest/v1/ ``` ### Done when - [ ] The API answers over HTTPS with a valid certificate - [ ] The Studio port is refused from another machine - [ ] Studio works through an SSH tunnel and can list tables - [ ] HTTP redirects to HTTPS ### Watch out - Studio talks to the stack with the service role key, which bypasses every row level security policy you will ever write. Exposing it to the internet behind only a password makes that password equivalent to the whole database. ## Phase 4 · Backups of both things that matter The database and the uploaded files both leave the server on a schedule. ### Steps 1. Dump the database on a schedule pg_dump from a container or from the host, gzipped and dated, run by cron or a systemd timer. ```sh docker compose exec -T db pg_dump -U postgres postgres | gzip > db-$(date +%F).sql.gz ``` 2. Archive the storage volume Tar the storage volume to the same destination with the same date stamp so the two halves of a restore are obviously paired. ```sh tar czf storage-$(date +%F).tar.gz -C ./volumes storage ``` 3. Ship it off the machine and alert on absence Copy to object storage or another host. Then check each morning that a file with today's date exists, and alert when it does not. Files: `scripts/backup.sh`, `scripts/check_backup.sh` ### Done when - [ ] A dated database dump and a dated storage archive both land off the server nightly - [ ] The dump is a valid gzip and its size is plausible for the data volume - [ ] A missing backup produces an alert - [ ] Several generations are retained rather than one file being overwritten ### Watch out - A database dump does not contain uploaded files, and a storage archive does not contain the database. Backing up only one gives you a restore that produces rows pointing at files that are gone, or files nobody can find. Back up both, on the same schedule, and restore both in the drill. ## Phase 5 · The restore drill, actually performed You have brought the whole thing back on a different server, and you know how long it took. ### Steps 1. Stand up a clean second server Same Docker setup, fresh clone of the stack, and the same environment file, which means the secrets are part of your backup and not only in your head. 2. Restore both halves Load the dump into the fresh database and unpack the storage archive into the storage volume before starting the storage service. ```sh gunzip -c db-2026-09-07.sql.gz | docker compose exec -T db psql -U postgres postgres ``` 3. Verify and write it down Compare row counts, download a file that existed before the backup, and sign in with an existing account. Then record the date and how long it took. Files: `README.md` ### Done when - [ ] Row counts on the restored server match the source - [ ] A file that existed before the backup downloads from the restored stack - [ ] An existing user can authenticate against the restored stack - [ ] The README records the date and duration of this drill ### Watch out - Restoring onto the same server proves almost nothing, because the parts you forgot are still sitting there. The drill has value precisely because the second machine has none of your undocumented state. ## Not in this build - A global edge network and the latency that comes with one. - Managed database operations, patching, tuning and failover. - Autoscaling, read replicas and connection pooling at scale. - Point in time recovery. Nightly dumps mean a bad day can cost you a day. - Anyone to call when it breaks. ## After v1, if you want it - Point in time recovery with continuous archiving, once nightly dumps stop feeling sufficient - A staging stack restored from last night's backup, which also exercises the restore weekly - Read replicas, but only after measuring that one server is genuinely the limit
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Generate with openssl rand. Never the shipped example. POSTGRES_PASSWORD=generate-a-long-random-value # Required · secret. Generate it, then derive the anon and service role keys from it. It is part of a working restore, so back it up. JWT_SECRET=generate-a-long-random-value # Required. Signed with your JWT secret. Public by design, but it must be yours rather than the published example. ANON_KEY=eyJhbGciOi... # Required · secret. Signed with your JWT secret. Bypasses row level security entirely, so it never reaches a browser. SERVICE_ROLE_KEY=eyJhbGciOi... # Required · secret. Studio's own login. Change it even though Studio is not publicly exposed. DASHBOARD_PASSWORD=generate-a-long-random-value # Required. Where auth redirects send users after sign-in. SITE_URL=https://app.example.com # Required. The off-server destination for both nightly artefacts. BACKUP_DEST=s3://your-bucket/supabase
# Supabase Pro · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Supabase Pro, self-host a Postgres backend with auth, storage, and a small API surface. The hard boundary is managed postgres, backups, auth delivery, storage, edge functions, observability, and support, plus infrastructure scale, operations, and reliability. ## Product outcome A Supabase stack you own end to end, with no default credential anywhere, a dashboard that is not exposed, both halves of the data backed up nightly off the machine, and a restore you have performed on a different server and dated. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Linux server - Docker - domain and DNS access - backup destination - basic systems administration ## Explicit non-goals for v1 - A global edge network and the latency that comes with one. - Managed database operations, patching, tuning and failover. - Autoscaling, read replicas and connection pooling at scale. - Point in time recovery. Nightly dumps mean a bad day can cost you a day. - Anyone to call when it breaks. - managed Postgres, backups, auth delivery, storage, edge functions, observability, and support - global edge network - managed databases - autoscaling - DDoS protection, support, and compliance ## Success criteria - No published example secret remains anywhere in the configuration - Studio is confirmed unreachable from another machine - Both backup artefacts exist off the server and are dated - A restore has been completed on a second server and the date recorded - Alerts have been tested by causing the condition, not by reading the configuration
# Build brief · Supabase Pro The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a self-hosted Supabase stack on one server you own, with backups you have actually restored to replace Supabase Pro. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Do not write a backend as a service. Supabase publishes its own Docker Compose stack and it is the thing to deploy. This is an operations task, not a build task: every phase below is deploy, configure and verify. If you find yourself writing an auth server or a storage API, stop and go back to the compose file. The last phase is a restore drill and it must actually be performed, with the date written down. A backup you have never restored is a belief, not a backup. ### Stack (fixed, do not substitute) - Platform: The official supabase/docker Compose stack · thirteen services that already work together; assembling them yourself is a month and a worse result - Host: One Linux server with at least 4 GB of memory and 2 virtual CPUs · the documented floor; 8 GB is the realistic figure once you have data and any traffic - Container runtime: Docker Engine 20.10 or newer with Compose v2 · what the stack's compose file expects - Gateway: Kong, which the stack already includes, listening on port 8000 · one entry point in front of PostgREST, auth, storage and realtime - Edge: Caddy in front of Kong for certificates and hostnames · automatic TLS, and a place to refuse traffic to Studio - Backups: pg_dump plus a copy of the storage volume, sent off the machine · the database and the uploaded files are two separate things and a dump contains only one of them ### Data model (create this before Phase 1) - You are not designing a schema here. What you are designing is the operational state, and it is worth writing down before you start. - secrets · POSTGRES_PASSWORD, JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY, DASHBOARD_PASSWORD, all generated by you and none of them the shipped example values - volumes · the PostgreSQL data directory and the storage volume, which are the two things whose loss is unrecoverable - backups · dated database dumps and dated storage archives, in a destination that is not this server - restore log · the date of every restore drill you have performed, kept in the README ### Phase 1 · Server, Docker and a closed front door Build: Provision a server with at least 4 GB of memory. Install Docker Engine and the compose plugin. Create a non-root user. Configure the firewall to allow only SSH and the web ports, and confirm from another machine that the database port is not reachable. Trap: Docker publishes ports by writing iptables rules that bypass the usual firewall front end, so a service you thought was blocked can be wide open. Verify with a port scan from a different machine rather than by reading your firewall configuration. Done when: Docker runs a test container as the non-root user. A port scan from another machine shows only SSH and the web ports open. Port 5432 is not reachable from outside the server. Do not build yet: No Supabase yet. A correctly closed server first. ### Phase 2 · The stack up, with secrets that are yours Build: Clone the supabase/docker directory, copy the example environment file, and replace every secret with a generated value. Generate the anon and service role keys from your own JWT secret. Bring the stack up and confirm every service reports healthy. Trap: The single most common self-hosting mistake is leaving the example values in place. The shipped anon key, service role key, JWT secret and dashboard password are published in the repository, so a stack running with them is readable and writable by anyone who finds it. Generate all of them before the first time the port is reachable, not after. Done when: Every container reports healthy. Grepping the environment file for the published example values returns nothing. A request signed with the example anon key is rejected. The service role key is not present in any file outside the environment file and your secret store. Do not build yet: No TLS and no public hostname yet. Keep it bound to localhost until the next phase. ### Phase 3 · TLS, hostnames and a private Studio Build: Point a domain at the server. Put Caddy in front of Kong so certificates are automatic. Expose the API host publicly. Do not expose Studio: bind it to localhost and reach it through an SSH tunnel, or put it behind an allowlist and its own authentication. Trap: Studio talks to the stack with the service role key, which bypasses every row level security policy you will ever write. Exposing it to the internet behind only a password makes that password equivalent to the whole database. Done when: The API answers over HTTPS with a valid certificate. Studio is not reachable from another machine, verified by trying. Studio is reachable through the tunnel and can list tables. A plain HTTP request is redirected to HTTPS. Do not build yet: No backups yet, which is the next phase and the important one. ### Phase 4 · Backups of both things that matter Build: A nightly pg_dump of the database and a nightly archive of the storage volume, both sent to a destination that is not this server, both dated, with several generations kept. Alert if a night passes with no new backup. Trap: A database dump does not contain uploaded files, and a storage archive does not contain the database. Backing up only one gives you a restore that produces rows pointing at files that are gone, or files nobody can find. Back up both, on the same schedule, and restore both in the drill. Done when: Two dated artefacts appear off the server each night. The database dump can be listed and its size is plausible. Deleting the newest dump and waiting a night produces a replacement and no alert; suppressing the job for a night produces an alert. Do not build yet: Do not call this done until Phase 5 has actually restored these artefacts. ### Phase 5 · The restore drill, actually performed Build: Provision a second server. Bring up a fresh stack. Load the newest database dump and unpack the newest storage archive. Verify row counts and that a stored file downloads. Write the date, the duration and anything that surprised you in the README. Trap: Restoring onto the same server proves almost nothing, because the parts you forgot are still sitting there. The drill has value precisely because the second machine has none of your undocumented state. Done when: A second server serves the same data. Row counts for the largest tables match the source. A file uploaded before the backup downloads correctly from the restored stack. Authentication works, meaning the JWT secret was part of what you restored. The date and duration are in the README. Do not build yet: Do not treat monitoring as a substitute for having done this. ### Phase 6 · Monitoring, upgrades and the incident you will have Build: Uptime checks against the API and the database. Disk alerts, because a full disk is the most common way this stack stops. A documented upgrade procedure that takes a backup first. Log retention that does not itself fill the disk. Trap: The stack's containers are versioned together and upgrading one service in isolation is how you get a database that the rest of the stack cannot talk to. Upgrade by moving the whole compose file forward, after a backup, and read the release notes for migration steps. Done when: An uptime check alerts within minutes when the API stops answering, tested by stopping it. A disk alert fires at a threshold that leaves room to act, tested by filling a scratch file. An upgrade has been performed on the second server first and only then on the live one. Do not build yet: Nothing after this. ### Out of scope (and why) - The global edge network. Your stack is on one machine in one place, and a visitor on another continent will feel that. - Managed database operations. Patching, tuning, connection limits and failover are now yours, and they are the largest part of what the subscription buys. - Autoscaling and read replicas. One server does not scale sideways, and pretending otherwise is how outages happen. - Point in time recovery. Nightly dumps mean you can lose up to a day; managed plans offer far tighter recovery windows and that difference is real. - Support. When something breaks at two in the morning, the person on call is you. ### README must contain - The server specification and why 4 GB is a floor rather than a target - That every shipped example secret was replaced, and where the real ones are stored - That Studio is not publicly reachable and exactly how to reach it - The backup schedule, both artefacts, the destination and the retention - The date and duration of the last restore drill, and the upgrade procedure
# Architecture · Supabase Pro ## Stack | Part | Choice | Why | | --- | --- | --- | | Platform | The official supabase/docker Compose stack | thirteen services that already work together; assembling them yourself is a month and a worse result | | Host | One Linux server with at least 4 GB of memory and 2 virtual CPUs | the documented floor; 8 GB is the realistic figure once you have data and any traffic | | Container runtime | Docker Engine 20.10 or newer with Compose v2 | what the stack's compose file expects | | Gateway | Kong, which the stack already includes, listening on port 8000 | one entry point in front of PostgREST, auth, storage and realtime | | Edge | Caddy in front of Kong for certificates and hostnames | automatic TLS, and a place to refuse traffic to Studio | | Backups | pg_dump plus a copy of the storage volume, sent off the machine | the database and the uploaded files are two separate things and a dump contains only one of them | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Compose stack | Every Supabase service and how they talk to each other. | Upgrade by moving the whole compose file forward together. Individual service versions are not independently swappable and treating them as such breaks the stack. | | Edge | TLS, hostnames and what is publicly reachable. | Caddy, nginx or Traefik are interchangeable. What must not change is that Studio stays private. | | Secrets | The five generated credentials. | Rotating the JWT secret invalidates existing tokens and is a planned event, not a maintenance task. | | Backups | Nightly database and storage artefacts sent off the machine. | The destination is replaceable. The rule that both artefacts are taken together is not. | | Monitoring | Knowing before your users do. | Any uptime service. The checks that matter are the API, a real database query, and free disk. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `POSTGRES_PASSWORD` · required, secret · Generate with openssl rand. Never the shipped example. - `JWT_SECRET` · required, secret · Generate it, then derive the anon and service role keys from it. It is part of a working restore, so back it up. - `ANON_KEY` · required · Signed with your JWT secret. Public by design, but it must be yours rather than the published example. - `SERVICE_ROLE_KEY` · required, secret · Signed with your JWT secret. Bypasses row level security entirely, so it never reaches a browser. - `DASHBOARD_PASSWORD` · required, secret · Studio's own login. Change it even though Studio is not publicly exposed. - `SITE_URL` · required · Where auth redirects send users after sign-in. - `BACKUP_DEST` · required · The off-server destination for both nightly artefacts. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Supabase Pro product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: The official supabase/docker Compose stack, One Linux server with at least 4 GB of memory and 2 virtual CPUs, Docker Engine 20.10 or newer with Compose v2, Kong, which the stack already includes, listening on port 8000, Caddy in front of Kong for certificates and hostnames, pg_dump plus a copy of the storage volume, sent off the machine. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Docker publishes ports by writing iptables rules that bypass the usual firewall front end, so a service you thought was blocked can be wide open. Verify with a port scan from a different machine rather than by reading your firewall configuration. - The single most common self-hosting mistake is leaving the example values in place. The shipped anon key, service role key, JWT secret and dashboard password are published in the repository, so a stack running with them is readable and writable by anyone who finds it. Generate all of them before the first time the port is reachable, not after. - Studio talks to the stack with the service role key, which bypasses every row level security policy you will ever write. Exposing it to the internet behind only a password makes that password equivalent to the whole database. - A database dump does not contain uploaded files, and a storage archive does not contain the database. Backing up only one gives you a restore that produces rows pointing at files that are gone, or files nobody can find. Back up both, on the same schedule, and restore both in the drill. - Restoring onto the same server proves almost nothing, because the parts you forgot are still sitting there. The drill has value precisely because the second machine has none of your undocumented state. - The stack's containers are versioned together and upgrading one service in isolation is how you get a database that the rest of the stack cannot talk to. Upgrade by moving the whole compose file forward, after a backup, and read the release notes for migration steps.
# Delivery milestones · Supabase Pro Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Server, Docker and a closed front door A server exists, runs containers, and exposes nothing to the internet that you have not decided to expose. ### Steps 1. Provision and harden the server Create a non-root user with sudo, disable password authentication for SSH, and enable unattended security updates. ```sh adduser deploy && usermod -aG sudo deploy systemctl status ssh ``` 2. Install Docker Engine and Compose v2 Use Docker's own repository rather than a distribution package, which is often several versions behind. ```sh docker --version docker compose version ``` 3. Close the firewall and verify from outside Allow SSH, 80 and 443 only. Then scan from your laptop. Trusting the configuration file is how people expose databases. ```sh ufw allow OpenSSH && ufw allow 80,443/tcp && ufw enable nmap -Pn YOUR_SERVER_IP ``` ### Done when - [ ] docker compose version reports v2 or higher - [ ] A scan from another machine shows only the ports you intended - [ ] Port 5432 is unreachable from outside the server - [ ] SSH password authentication is disabled ### Watch out - Docker publishes ports by writing iptables rules that bypass the usual firewall front end, so a service you thought was blocked can be wide open. Verify with a port scan from a different machine rather than by reading your firewall configuration. ## M2 · The stack up, with secrets that are yours Supabase runs, and not one credential is the value that shipped in the example file. ### Steps 1. Clone the stack and prepare the environment file Take only the docker directory. Copy .env.example to .env, then treat every line of it as something you must replace. Files: `docker/.env` ```sh git clone --depth 1 https://github.com/supabase/supabase cp supabase/docker/.env.example supabase/docker/.env ``` 2. Generate every secret A long random PostgreSQL password, a long random JWT secret, then derive the anon and service role keys by signing the documented payloads with that secret. Store them in a password manager as you go. ```sh openssl rand -base64 48 ``` 3. Bring it up and check health Start the stack and read the health of every container. A container in a restart loop is the normal first outcome and the logs say why. ```sh docker compose up -d docker compose ps ``` ### Done when - [ ] Every container reports healthy rather than restarting - [ ] No published example secret appears anywhere in the environment file - [ ] A request using the example anon key is rejected - [ ] The stack survives a full server reboot and comes back healthy ### Watch out - The single most common self-hosting mistake is leaving the example values in place. The shipped anon key, service role key, JWT secret and dashboard password are published in the repository, so a stack running with them is readable and writable by anyone who finds it. Generate all of them before the first time the port is reachable, not after. ## M3 · TLS, hostnames and a private Studio The API is reachable over HTTPS at your domain, and the dashboard is not reachable by the public at all. ### Steps 1. Point DNS and run Caddy in front of Kong An A record for the API hostname. Caddy proxies to Kong on 8000 and obtains certificates on first request. Files: `Caddyfile` 2. Keep Studio off the public internet Bind its published port to 127.0.0.1 in the compose override, then reach it with ssh -L when you need it. Files: `docker-compose.override.yml` ```sh ssh -L 3000:localhost:3000 deploy@YOUR_SERVER ``` 3. Verify from outside, not from the server Curl the API host from your laptop, and try the Studio port from your laptop too. The second one must fail. ```sh curl -I https://api.example.com/rest/v1/ ``` ### Done when - [ ] The API answers over HTTPS with a valid certificate - [ ] The Studio port is refused from another machine - [ ] Studio works through an SSH tunnel and can list tables - [ ] HTTP redirects to HTTPS ### Watch out - Studio talks to the stack with the service role key, which bypasses every row level security policy you will ever write. Exposing it to the internet behind only a password makes that password equivalent to the whole database. ## M4 · Backups of both things that matter The database and the uploaded files both leave the server on a schedule. ### Steps 1. Dump the database on a schedule pg_dump from a container or from the host, gzipped and dated, run by cron or a systemd timer. ```sh docker compose exec -T db pg_dump -U postgres postgres | gzip > db-$(date +%F).sql.gz ``` 2. Archive the storage volume Tar the storage volume to the same destination with the same date stamp so the two halves of a restore are obviously paired. ```sh tar czf storage-$(date +%F).tar.gz -C ./volumes storage ``` 3. Ship it off the machine and alert on absence Copy to object storage or another host. Then check each morning that a file with today's date exists, and alert when it does not. Files: `scripts/backup.sh`, `scripts/check_backup.sh` ### Done when - [ ] A dated database dump and a dated storage archive both land off the server nightly - [ ] The dump is a valid gzip and its size is plausible for the data volume - [ ] A missing backup produces an alert - [ ] Several generations are retained rather than one file being overwritten ### Watch out - A database dump does not contain uploaded files, and a storage archive does not contain the database. Backing up only one gives you a restore that produces rows pointing at files that are gone, or files nobody can find. Back up both, on the same schedule, and restore both in the drill. ## M5 · The restore drill, actually performed You have brought the whole thing back on a different server, and you know how long it took. ### Steps 1. Stand up a clean second server Same Docker setup, fresh clone of the stack, and the same environment file, which means the secrets are part of your backup and not only in your head. 2. Restore both halves Load the dump into the fresh database and unpack the storage archive into the storage volume before starting the storage service. ```sh gunzip -c db-2026-09-07.sql.gz | docker compose exec -T db psql -U postgres postgres ``` 3. Verify and write it down Compare row counts, download a file that existed before the backup, and sign in with an existing account. Then record the date and how long it took. Files: `README.md` ### Done when - [ ] Row counts on the restored server match the source - [ ] A file that existed before the backup downloads from the restored stack - [ ] An existing user can authenticate against the restored stack - [ ] The README records the date and duration of this drill ### Watch out - Restoring onto the same server proves almost nothing, because the parts you forgot are still sitting there. The drill has value precisely because the second machine has none of your undocumented state. ## M6 · Monitoring, upgrades and the incident you will have (production only) You find out about problems before your users do, and an upgrade is a planned event. ### Steps 1. Monitor the API, the database and the disk An external uptime check for the API, a query check for the database, and a disk threshold at around seventy five percent so there is time to react. 2. Write the upgrade procedure Back up, pull the new compose file, read the release notes, apply on the second server, verify, then apply on the live one. Written down, because you will do this while distracted. Files: `docs/upgrade.md` 3. Cap log growth Set the Docker logging driver's max size and file count, or the container logs will quietly consume the disk over months. Files: `docker-compose.override.yml` ### Done when - [ ] Stopping the API triggers an alert within minutes - [ ] A disk threshold alert fires when tested with a scratch file - [ ] Container logs are capped and cannot grow without bound - [ ] An upgrade was rehearsed on the second server before the live one ### Watch out - The stack's containers are versioned together and upgrading one service in isolation is how you get a database that the rest of the stack cannot talk to. Upgrade by moving the whole compose file forward, after a backup, and read the release notes for migration steps.
# Operations · Supabase Pro ## Backup Nightly pg_dump and a nightly storage volume archive, dated, several generations, off the server. The environment file holding the JWT secret belongs in your secret store, because a restore without it cannot authenticate existing users. ## Restore Fresh server, fresh stack, same environment file, load the dump, unpack the storage archive, verify row counts and a file download, then sign in as an existing user. Perform this on a schedule and record each date. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring External uptime on the API, a query-level database check, disk usage with an alert at around seventy five percent, and a morning check that last night's backup exists. Disk is the one that actually bites. ## Incident checklist For a failed upgrade, restore the pre-upgrade dump onto a clean stack rather than trying to repair in place. For a disk full event, the storage volume and the container logs are the usual causes, in that order. For a leaked service role key, rotate the JWT secret and accept that every existing session ends. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] No published example secret remains anywhere in the configuration - [ ] Studio is confirmed unreachable from another machine - [ ] Both backup artefacts exist off the server and are dated - [ ] A restore has been completed on a second server and the date recorded - [ ] Alerts have been tested by causing the condition, not by reading the configuration ## Launch constraint Do not market omitted Supabase Pro capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Generate with openssl rand. Never the shipped example. POSTGRES_PASSWORD=generate-a-long-random-value # Required · secret. Generate it, then derive the anon and service role keys from it. It is part of a working restore, so back it up. JWT_SECRET=generate-a-long-random-value # Required. Signed with your JWT secret. Public by design, but it must be yours rather than the published example. ANON_KEY=eyJhbGciOi... # Required · secret. Signed with your JWT secret. Bypasses row level security entirely, so it never reaches a browser. SERVICE_ROLE_KEY=eyJhbGciOi... # Required · secret. Studio's own login. Change it even though Studio is not publicly exposed. DASHBOARD_PASSWORD=generate-a-long-random-value # Required. Where auth redirects send users after sign-in. SITE_URL=https://app.example.com # Required. The off-server destination for both nightly artefacts. BACKUP_DEST=s3://your-bucket/supabase
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for Supabase Pro because hosting products sell an operations team and failure-domain diversity, not merely a deploy button. The recurring cost buys patching, certificates, isolation, secrets, builds, deploys, logs, metrics, backups, capacity, incidents, and security response, not just the visible interface.
xmanaged Postgres, backups, auth delivery, storage, edge functions, observability, and support
xglobal edge network
xmanaged databases
xautoscaling
xDDoS protection, support, and compliance
Don't feel like building it? These folks already made it free.
all 4 free alternatives to Supabase Pro →· no votes, no pay-to-list · just what's real
Supabase Pro pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0/workspace | — | 2 active projects; 50,000 MAU; 500 MB database/project; 1 GB storage; 5 GB egress + 5 GB cached egress; 2 million Realtime messages |
| pro | $25/workspace | — | 100,000 MAU; 8 GB database disk/project; 100 GB storage; 250 GB egress + 250 GB cached egress; 7-day backups/log retention |
| team | $599/workspace | — | Pro usage allowances plus team controls, SSO, audit logs and longer operational support features |
| enterprise | custom | — | Custom usage, support, compliance and deployment terms |
free tier2 active projects; 50,000 MAU; 500 MB database/project; 1 GB file storage; 5 GB egress + 5 GB cached egress; 50 MB maximum file; 200 Realtime connections; 2 million Realtime messages; 500,000 Edge Function invocations
billingmonthly self-serve billing; no public annual self-serve price shown
hidden costsThe $25 Pro fee is not the whole bill: each project consumes compute after the included $10 credit; overages include MAU, storage, egress, database disk, Realtime, Edge Functions and image transforms; paid add-ons include custom domains ($10/month), PITR (from $100/month), phone MFA and log drains
verified 2026-08-14 · source ↗
Vibecode Supabase Pro
Kinda. The core of Supabase Pro is buildable in a weekend with the prompt on this page, but there are real gaps: managed Postgres, backups, auth delivery, storage, edge functions, observability, and support, global edge network. Read the honest list above before committing.
How much does Supabase Pro cost?
Supabase Pro's pricing is usage-based or varies by plan · Official Pro monthly project price before usage..
What do I lose by replacing Supabase Pro?
Honestly: managed Postgres, backups, auth delivery, storage, edge functions, observability, and support; global edge network; managed databases; autoscaling; DDoS protection, support, and compliance. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Supabase Pro?
Yes: Supabase (The actual Postgres, Auth, Storage, Realtime, Functions, and API stack; free software, decidedly non-free operations.) PocketBase (A drastically smaller substitute for one app: auth, files, realtime, API, and database in one binary, with no functions runtime.) Appwrite (A similarly complete backend console with auth, data, files, realtime, and functions; the database is not Postgres.) All 4 curated free alternatives are at vibecodeit.com/supabase-pro/alternatives. The prompt is for when you want it exactly your way.