---
title: Quickstart
description: Get both halves of TALA running against a local Postgres, including the connection traps that cost the most time.
sidebar:
  icon: rocket
  order: 1
---

Ten minutes to a working local stack, assuming you already have Postgres and the credentials.

## Prerequisites

- **Node v22.16.0** — pinned in `backend/.nvmrc`. Newer works; older does not.
- **PostgreSQL** running locally, or a Supabase connection string.
- **AWS** S3 bucket, CloudFront distribution, and Lambda credentials.
- **Resend** API key, plus `MAIL_FROM_NAME` and `MAIL_FROM_EMAIL` on a verified domain.
- **Google and GitHub OAuth** client credentials — optional; everything else works without them.

## Backend

1. **Copy the environment file**

    ```bash
    cd backend
    cp .env.example .env
    ```

    `.env.example` is the authoritative reference for every variable, and it documents the connection-string traps below. Read it before debugging a database failure.

2. **Fill in the required values**

    Two variables are checked at boot, and the app refuses to start if either fails:

    - **`SECRET_KEY`** — at least 32 characters, and must not contain `change-me`. `.env.example` ships exactly that placeholder, so **you must replace it** or the boot check fails.
    - **`BUFFER_KEY`** — exactly 64 hex characters.

    `DATABASE_URL` is not part of that check but is obviously required; a bad one fails at the first query instead of at boot. Add the AWS, Resend, and OAuth credentials as you need them.

    ```bash
    # Both of these must be replaced, not left as shipped.
    SECRET_KEY=$(openssl rand -hex 32)
    BUFFER_KEY=$(openssl rand -hex 32)
    ```

    A Homebrew-default local Postgres is built without SSL, so it also needs:

    ```bash
    DB_SSL=false
    ```

3. **Install, migrate, run**

    ```bash
    npm install
    npm run migration:run
    npm run start:dev
    ```

The API listens on **port 8000**. Swagger UI is at `http://localhost:8000/api-docs` and the liveness probe at `http://localhost:8000/health` — note that `/health` sits *outside* the `api` global prefix.

:::success[Plans are already seeded]
The `SeedPlans` migration inserts them. You do not need to call `/api/seeder` on a fresh database — and you could not anyway without an ADMIN account.
:::

## Frontend

```bash
cd frontend
echo "VITE_API_URL=http://localhost:8000" > .env
npm install
npm run dev
```

Vite serves on `http://localhost:5173`, which is also the CORS fallback origin the backend allows when `FRONTEND_URL` is unset.

:::warning[`VITE_API_URL` has no remote fallback, on purpose]
`API_BASE_URL` falls back to `http://localhost:8000` and nothing else. A build with a missing or misspelt `VITE_API_URL` fails visibly instead of silently shipping against the wrong backend — which is what happened when it used to default to the Render instance.
:::

## Supabase connection traps

Shared environments run on Supabase. Use the **session pooler**, never the direct host:

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

<Accordion>
  <AccordionItem title="The direct host is IPv6-only">
    `db.<ref>.supabase.co` publishes an AAAA record and no A record. It is unroutable from most laptops and IPv4 hosts unless you pay for the IPv4 add-on.
  </AccordionItem>
  <AccordionItem title="Port 6543 breaks TypeORM">
    5432 is session mode and is the one you want. 6543 is transaction mode, which breaks TypeORM's prepared statements.
  </AccordionItem>
  <AccordionItem title="The username is not plain postgres">
    It is `postgres.<project-ref>`. Newer projects also use the `aws-1-` region prefix, not `aws-0-`.
  </AccordionItem>
  <AccordionItem title="Never set DB_SSL=false against Supabase">
    That flag is a local-Postgres-only escape hatch for a server built without SSL. Against a hosted database it disables transport encryption outright.
  </AccordionItem>
</Accordion>

## Row-level security is not automatic

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 any of this, so tables created by a migration land wide open.

The `LockDownPublicSchema` migration enables RLS on every public table and revokes those roles' grants **and default privileges**.

:::danger[Run it on every new Supabase project]
It is a migration, not a platform setting. A fresh project without it is readable and writable by anyone holding the public `anon` key.
:::

## Regenerating the API reference

The [API reference](/reference) in these docs is built from `docs/openapi.json`, which is a snapshot of the backend's live Swagger document. Refresh it whenever a controller changes:

```bash
cd backend && npm run start:dev   # terminal 1
cd docs    && npm run spec        # terminal 2
```

Then rebuild the docs:

```bash
cd docs && npm run build
```

:::warning[Use `npm run spec`, not a bare `curl`]
The script runs from `docs/`, so the file lands beside `blume.config.ts`, and it passes `curl -sf` so an error page is never written over a working spec. Running `curl … > docs/openapi.json` from inside `backend/` instead drops the file into `backend/docs/` — a real directory holding the backend's own design notes — and leaves the reference untouched.
:::
