> ## Documentation Index
> Fetch the complete documentation index at: https://atlas.rebuildingus.org/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Self-hosting guide

> Deploy your own Atlas instance using the recommended topology or Docker Compose.

## Overview

Atlas is open source and designed to be self-hosted. This guide covers the recommended deployment topology and configuration.

<Note>
  Most users don't need to self-host. The hosted version at
  [atlas.rebuildingus.org](https://atlas.rebuildingus.org) is the easiest way to
  use Atlas. Self-hosting is for organizations that need full control over their
  data, want to run Atlas on their own infrastructure, or want to contribute to
  development.
</Note>

## Recommended topology

The simplest production setup is:

* **Vercel** for the web app (`app/`)
* **A small VM** for the API (`api/`) running in Docker
* **SQLite** on a persistent volume for data storage

This is the architecture Atlas uses in production. It's cost-effective, easy to operate, and works well for a single operator.

**Why this split:**

* Vercel handles the frontend well and gives you automatic deploys and rollbacks
* The API uses SQLite and runs background discovery work, which is better suited to a persistent VM
* This is usually cheaper and simpler than putting everything on one platform

## Quick start

### 1. Clone the repository

```bash theme={null}
git clone https://github.com/RebuildingAmerica/atlas.git
cd atlas
```

### 2. Run the bootstrap script

```bash theme={null}
pnpm bootstrap
```

This creates your `.env.production` file, generates auth secrets, and sets up the initial configuration. Follow the prompts to fill in required values.

### 3. Configure Stripe billing

Atlas uses Stripe Checkout, Stripe Billing, coupons, and webhooks for Pro, Team,
and Research Pass access. The bootstrap scripts create the canonical products,
prices, discount coupons, and webhook endpoints, then write the generated Stripe
catalog to the correct env files.

Local development uses Stripe test mode. Keep Stripe's webhook forwarder open
while testing Checkout. The listener reads the local app origin from `.env`,
defaults to `https://atlas.localhost`, and forwards Stripe billing events to
`/api/stripe/webhook`:

```bash theme={null}
pnpm setup:local
pnpm stripe:listen
pnpm stripe:verify:local
```

Hosted staging uses Stripe test mode and Vercel Preview env vars:

```bash theme={null}
pnpm setup:staging --yes
pnpm stripe:verify:staging
```

Hosted production uses Stripe live mode and Vercel Production env vars:

```bash theme={null}
STRIPE_API_KEY=rk_live_replace_me pnpm setup:prod --yes
STRIPE_API_KEY=rk_live_replace_me pnpm stripe:verify:prod
```

To create the live key, open Stripe Dashboard in Live mode, go to **Developers >
API keys > Restricted keys**, create a key named `Atlas Production Billing`,
set these permissions, reveal the key once, and run setup with that `rk_live_...`
value:

* **Read:** Accounts v2, shown by Stripe errors as Basic Business Contact
  Information (`accounts_kyc_basic_read`)
* **Write:** Products, Prices, Coupons, Customers, Checkout Sessions, Webhook
  Endpoints

`Accounts v2: Read` lets bootstrap show the friendly Stripe account name. If
the restricted-key UI does not expose that permission, bootstrap can continue
after Stripe returns the account ID and you confirm it is the intended account.
A live secret key also works, but restricted keys are the default production
path.

Stripe CLI OAuth keys are enough for local and staging checks, but production
bootstrap should use a Dashboard-created live API key. Test-mode IDs cannot be
used in production.

The catalog is intentionally small: Pro, Team, Team Seat, and Research Pass.
Discounts are Pro-only coupons. Verified students pay \$12.80 every four months;
independent creators and journalists get 50% off Pro monthly or annual billing.
Team pricing stays the same for every organization. The verification commands
check products, prices, coupon scope, hosted Vercel env presence, and hosted
webhook endpoint drift.

See the repository's
[`docs/deployment/stripe-billing.md`](https://github.com/RebuildingAmerica/atlas/blob/main/docs/deployment/stripe-billing.md)
runbook for the exact product catalog, discount policy, and verification
commands.

### 4. Choose a deployment path

<Tabs>
  <Tab title="Vercel + Docker (recommended)">
    **Deploy the web app to Vercel:**

    Create a Vercel project with these settings:

    | Setting          | Value                            |
    | ---------------- | -------------------------------- |
    | Framework Preset | TanStack Start                   |
    | Root Directory   | `app`                            |
    | Install Command  | `pnpm install --frozen-lockfile` |
    | Build Command    | `pnpm run build`                 |
    | Node Version     | 24                               |

    Set these environment variables in the Vercel dashboard:

    ```env theme={null}
    ATLAS_PUBLIC_URL=https://your-atlas-domain.com
    ATLAS_DOCS_URL=https://your-subdomain.mintlify.dev
    ATLAS_SERVER_API_PROXY_TARGET=https://api.your-atlas-domain.com
    ATLAS_AUTH_API_KEY_INTROSPECTION_URL=https://your-atlas-domain.com/api/auth/internal/api-key
    ATLAS_AUTH_INTERNAL_SECRET=<from .env.production>
    ATLAS_EMAIL_PROVIDER=resend
    ATLAS_EMAIL_FROM=Atlas <hello@mail.your-atlas-domain.com>
    ATLAS_EMAIL_RESEND_API_KEY=<your Resend API key>
    ```

    `pnpm setup:staging` and `pnpm setup:prod` sync the Stripe billing variables to
    the linked Vercel project automatically.

    **Deploy the API to your VM:**

    ```bash theme={null}
    cp .env.production.example .env.production
    ```

    Fill in `.env.production`, then start the API:

    ```bash theme={null}
    docker compose --env-file .env.production up -d --build atlas-api
    ```

    Expose the API through your reverse proxy or load balancer, then point Vercel's `ATLAS_SERVER_API_PROXY_TARGET` at the public API URL.
  </Tab>

  <Tab title="Docker full stack">
    Run everything on one machine with Docker Compose:

    ```bash theme={null}
    cp .env.production.example .env.production
    ```

    Fill in `.env.production`, then start the stack:

    ```bash theme={null}
    docker compose --env-file .env.production -f compose.yaml up -d --build
    ```

    This starts three services:

    * `app` — TanStack Start web server
    * `api` — FastAPI API server
    * `caddy` — Reverse proxy on ports 80 and 443

    Caddy routes requests automatically:

    * `/api/auth/*` goes to the app (Better Auth)
    * `/api/*` goes to the API
    * Everything else goes to the app

    Set `ATLAS_PUBLIC_URL=https://your-atlas-domain.com` in your `.env.production`.
  </Tab>
</Tabs>

## Environment variables

### Required

| Variable                     | Description                                                                                                                                                                                     |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ATLAS_PUBLIC_URL`           | Public origin of your Atlas instance (e.g., `https://atlas.example.com`)                                                                                                                        |
| `ATLAS_AUTH_INTERNAL_SECRET` | Shared secret between app and API. Generated by `pnpm bootstrap`.                                                                                                                               |
| `ATLAS_AUTH_DB_PATH`         | Path to Better Auth SQLite database. Must be on persistent storage.                                                                                                                             |
| `ATLAS_AUTH_JWT_AUDIENCES`   | OAuth audience claim for JWT validation. Include the MCP resource first, e.g. `https://atlas.example.com/mcp`, and add a comma-separated API resource when direct REST OAuth access is exposed. |
| `DATABASE_URL`               | Path to the API's SQLite database. Must be on persistent storage.                                                                                                                               |
| `CORS_ORIGINS`               | JSON array of allowed origins (e.g., `["https://atlas.example.com"]`)                                                                                                                           |

### Authentication

| Variable                               | Description                                                                         |
| -------------------------------------- | ----------------------------------------------------------------------------------- |
| `ATLAS_AUTH_API_KEY_INTROSPECTION_URL` | Internal URL for API key validation                                                 |
| `ATLAS_OPERATOR_ALLOWED_EMAILS`        | Comma-separated bootstrap allowlist for first owners (optional)                     |
| `ATLAS_DEPLOY_MODE`                    | Set to `local` for single-user local operation (disables auth). Omit in production. |

### Email

| Variable                     | Description                                       |
| ---------------------------- | ------------------------------------------------- |
| `ATLAS_EMAIL_PROVIDER`       | `resend` for production, `capture` for local/test |
| `ATLAS_EMAIL_FROM`           | Sender address for auth and transactional email   |
| `ATLAS_EMAIL_RESEND_API_KEY` | Resend API key (when using `resend` provider)     |

### Discovery

| Variable            | Description                                    |
| ------------------- | ---------------------------------------------- |
| `ANTHROPIC_API_KEY` | Required for the AI-powered discovery pipeline |
| `SEARCH_API_KEY`    | API key for web search during source fetching  |

## Persistent storage

Both SQLite databases (content and auth) **must** live on persistent storage that survives container restarts. In Docker, this means a named volume or a mounted host directory.

### Backups

At minimum:

1. Copy or snapshot the SQLite files on a schedule
2. Store backups somewhere other than the VM itself
3. Test a restore into a fresh volume

If you skip the restore test, you don't know whether your backup is useful.

## Verification checklist

After every deployment, check these in order:

1. `GET /health` returns 200
2. The app loads and can list entities
3. An entity detail page loads correctly
4. Magic-link sign-in succeeds
5. API key creation succeeds and `X-API-Key` access works
6. Creating a discovery run succeeds
7. Restarting the API does not lose data

## Adapting for other hosts

The Vercel + Docker topology maps to most hosting providers:

| Component        | Alternatives                                                  |
| ---------------- | ------------------------------------------------------------- |
| Vercel (app)     | Netlify, Cloudflare Pages, any Node.js host                   |
| Docker VM (API)  | Railway, Fly.io, Render, any container host                   |
| Caddy (proxy)    | Nginx, Traefik, cloud load balancers                          |
| SQLite (storage) | Keep SQLite on persistent disk; no external database required |

The key requirements:

* The app needs `ATLAS_PUBLIC_URL` and `ATLAS_SERVER_API_PROXY_TARGET`
* The API needs `DATABASE_URL` pointing to persistent SQLite storage
* Both services need the same `ATLAS_AUTH_INTERNAL_SECRET`
* The API container must support long-running background tasks (discovery pipeline)

<Warning>
  Serverless platforms (AWS Lambda, Vercel Functions) are not suitable for the
  API because discovery runs are long-lived background tasks. Use a persistent
  container or VM.
</Warning>

## What's next

<CardGroup cols={2}>
  <Card title="API guide" icon="code" href="/docs/api">
    Understand the API you're hosting.
  </Card>

  <Card title="Contributing" icon="github" href="/docs/resources/contributing">
    Help improve Atlas.
  </Card>
</CardGroup>
