---
title: Environments
description: Three environments mapped onto three branches, the TLS rules for each, and the Supabase connection traps and RLS lockdown that are not optional.
---

| | dev | staging | prod |
| --- | --- | --- | --- |
| Branch | `dev` | `staging` | `main` |
| Database | local Postgres `tala_dev` | Supabase (shared) | Supabase — **not created as of 2026-09-09** |
| `DB_SYNCHRONIZE` | may be `true` locally | **must be set to `false`** | forced `false` by `NODE_ENV` |
| `migrationsRun` | `false` | `false` | `false` |

Migrations are run explicitly in every environment. Nothing runs them at boot.

:::danger[Unset means `true` everywhere except production]
`DB_SYNCHRONIZE` is only forced off when `NODE_ENV=production`. On any other value — including `staging` — an unset or empty flag resolves to `true`, and TypeORM will alter the shared database's schema to match the entities at boot. `.env.example` ships the key empty, so this is the default state of a freshly copied `.env`. Set it to `false` explicitly on staging.
:::

## Environment validation

The backend validates its environment at startup via `validateEnv` and refuses to boot on a bad one. It checks exactly two variables:

| Variable | Rule |
| --- | --- |
| `SECRET_KEY` | at least 32 characters, and must not match `/change-?me/i` |
| `BUFFER_KEY` | exactly 64 hex characters |

`DATABASE_URL` is **not** validated — a missing or malformed connection string fails later, at the first query, not at boot.

:::warning[`.env.example` ships a `SECRET_KEY` that fails validation]
It contains the literal `change-me`, which the placeholder check rejects. Copying `.env.example` and filling in only `DATABASE_URL` leaves the app unable to start. Replace `SECRET_KEY` with a real value of 32+ characters.
:::

`.env.example` is the authoritative reference for everything else — read it before debugging a connection.

:::warning[`.env` drifts from `.env.example`]
`AWS_S3_BUCKET` and `AWS_S3_ENDPOINT` are required, and are the values most often missing from a local `.env`. Check them first when an upload fails.
:::

## TLS to the database

```ts
if (this._configService.get<string>("DB_SSL") === "false") return false;
```

`DB_SSL=false` is a **local-Postgres-only** escape hatch, for a Homebrew build compiled without SSL.

Outside production the connection also uses `ssl.rejectUnauthorized: false`. That is a permissive TLS path intended for local development, but it applies to *any* non-production connection — including a staging connection to a hosted database. On staging that means the traffic is encrypted but the database's certificate is never verified, so a substituted certificate would be accepted. Narrowing the flag to local Postgres is unowned.

In production, `DB_SSL_CA` may pin a CA as either an inline PEM or a path to one. If `DB_SSL_CA` is unset, the certificate is still validated against Node's bundled CAs and a warning is logged.

## Supabase

Use the **session pooler**, never the direct host:

```text
postgresql://postgres.<ref>:<password>@aws-1-<region>.pooler.supabase.com:5432/postgres
```

| Trap | Detail |
| --- | --- |
| Direct host is IPv6-only | `db.<ref>.supabase.co` has AAAA and no A record — unroutable from most laptops without the paid IPv4 add-on |
| Port | 5432 is session mode. 6543 is transaction mode and breaks TypeORM's prepared statements |
| Username | `postgres.<project-ref>`, not `postgres` |
| Region prefix | newer projects use `aws-1-`, not `aws-0-` |
| `DB_SSL=false` | Never against Supabase — it disables transport encryption outright |

## Row-level security

Supabase publishes the `public` schema through PostgREST and grants `anon` and `authenticated` full DML on everything created there. The `anon` key is public by design. TypeORM knows nothing about RLS, so tables created by a migration land wide open.

`LockDownPublicSchema` enables RLS on all public tables and revokes those roles' grants **and their default privileges**. The default privileges are the part people skip: leave them in place and the next table a migration creates re-opens the hole.

:::danger[Every new Supabase project must have this migration run against it]
It is a migration, not a platform setting, and it is not automatic. A fresh project without it is readable and writable by anyone holding the public `anon` key.
:::
