# Addons — Locus Build

> Companion guide to [`SKILL.md`](./SKILL.md). Covers Postgres/Redis provisioning, introspection, and execution.

## When To Load

Load this file only for Postgres or Redis provisioning, schema inspection, queries, or migration workflows.

## Table of Contents

- [Provisioning](#provisioning)
- [Agent Workflow: Addon Provisioning](#agent-workflow-addon-provisioning)
- [Database Introspection](#database-introspection)
- [Database Execution (Agent API)](#database-execution-agent-api)
- [Common Agent Database Workflows](#common-agent-database-workflows)

## Provisioning

Addons are managed databases and caches provisioned into your environment. Services connect to addons by explicitly referencing them in their environment variables using template syntax (e.g., `"DATABASE_URL": "${{addonName.DATABASE_URL}}"`).

> **Billing:** Each addon costs **$0.25/month**, deducted from the workspace credit balance on creation. New workspaces start with $1.00. If the workspace has insufficient credits, the API returns `402 Insufficient credits` — see [billing.md](./billing.md) for payment flow.

> **Explicit references required.** After provisioning an addon, add its variables to each service that needs them using template syntax: `"DATABASE_URL": "${{addonName.DATABASE_URL}}"`. Only services with explicit references will receive the addon's connection variables.

**Postgres template fields:**

| Template | Example value |
|----------|---------------|
| `${{addonName.DATABASE_URL}}` | `postgresql://appuser:pass@locus-postgres.xxx.rds.amazonaws.com:5432/appdb` |
| `${{addonName.HOST}}` | `locus-postgres.xxx.rds.amazonaws.com` |
| `${{addonName.PORT}}` | `5432` |
| `${{addonName.USERNAME}}` | `appuser` |
| `${{addonName.DATABASE}}` | `appdb` |

**Redis template fields:**

| Template | Example value |
|----------|---------------|
| `${{addonName.REDIS_URL}}` | `redis://locus-redis.xxx.cache.amazonaws.com:6379/0` |
| `${{addonName.HOST}}` | `locus-redis.xxx.cache.amazonaws.com` |
| `${{addonName.PORT}}` | `6379` |

**Expected timing:**
- Postgres provisioning: ~30-60 seconds (creates database + user in shared Postgres instance)
- Redis provisioning: ~10-20 seconds (allocates DB number in shared Redis cluster)
- Variable availability: After provisioning, add template references to services and redeploy

**Provision a Postgres database:**

```bash
ADDON=$(curl -s -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "'"$PROJECT_ID"'",
    "environmentId": "'"$ENV_ID"'",
    "type": "postgres",
    "name": "main-db",
    "config": {
      "databaseName": "appdb",
      "username": "appuser"
    }
  }' \
  "https://api.buildwithlocus.com/v1/addons")

ADDON_ID=$(echo $ADDON | jq -r '.id')
echo "Addon ID: $ADDON_ID"
```

**Database naming:** The provisioning Lambda auto-generates the database name and username based on the addon ID (e.g., `addon_xxx` / `user_addon_xxx`). The `databaseName` and `username` values in `config` are hints but may not match the actual provisioned names. Always use `${{addonName.DATABASE_URL}}` which contains the correct database name, host, and credentials — never construct the connection string manually.

**Provision a Redis cache:**

```bash
curl -s -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "'"$PROJECT_ID"'",
    "environmentId": "'"$ENV_ID"'",
    "type": "redis",
    "name": "cache"
  }' \
  "https://api.buildwithlocus.com/v1/addons"
```

**Poll addon until available:**

```bash
while true; do
  STATUS=$(curl -s -H "Authorization: Bearer $TOKEN" \
    "https://api.buildwithlocus.com/v1/addons/$ADDON_ID" \
    | jq -r '.status')
  echo "Addon status: $STATUS"
  [ "$STATUS" = "available" ] && break
  sleep 5
done
```

| Addon status | Meaning |
|-------------|---------|
| `provisioning` | Infrastructure being created |
| `available` | Ready to use; `connectionString` populated |
| `failed` | Provisioning failed |

**After addon is `available`:** Trigger a new deployment to inject the connection string into your service:

```bash
# Addon is now available - redeploy service to get DATABASE_URL
curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"serviceId": "'"$SERVICE_ID"'"}' \
  "https://api.buildwithlocus.com/v1/deployments"
```

The next deployment will automatically include `DATABASE_URL` as an environment variable in your container.

**Addon hints:**
- `POST /v1/addons` response includes `note` reminding to redeploy services after provisioning
- `GET /v1/addons/:id` response includes `requiresRedeploy` boolean

## Agent Workflow: Addon Provisioning

**For AI agents:** Addon provisioning is fast but has a non-obvious follow-up step (redeploying) that confuses humans if you don't explain it. Always narrate the full flow.

### Expected Timing

| Addon type | Provisioning time | What's happening |
|------------|-------------------|------------------|
| **Postgres** | 30-60 seconds | Creating database and user in shared Postgres instance |
| **Redis** | 10-20 seconds | Allocating DB number in shared Redis cluster |

### Recommended Workflow

**Step 1: Tell the human what you're doing**
```
Provisioning a Postgres database for your service. This takes about 30-60 seconds.
```

**Step 2: Provision and poll until available**
```bash
# Provision
ADDON_ID=$(curl -s -X POST ... | jq -r '.id')

# Poll every 5 seconds
while true; do
  STATUS=$(curl -s -H "Authorization: Bearer $TOKEN" \
    "https://api.buildwithlocus.com/v1/addons/$ADDON_ID" | jq -r '.status')
  [ "$STATUS" = "available" ] || [ "$STATUS" = "failed" ] && break
  sleep 5
done
```

**Step 3: Add addon reference to the service's environment variables**
```bash
curl -s -X PUT "https://api.buildwithlocus.com/v1/services/$SERVICE_ID/variables" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"DATABASE_URL": "${{addonName.DATABASE_URL}}"}'
```

**Step 4: Report and redeploy**
```
Database provisioned and DATABASE_URL configured! Redeploying so your service picks up the connection string.
```
Trigger a redeploy — env vars are injected at deploy time, so the service needs a fresh deployment to connect.

### Communication Best Practices

**DO:**
- Add addon references to the service's environment variables: `"DATABASE_URL": "${{addonName.DATABASE_URL}}"`
- Explain why a redeploy is needed (env vars are injected at deploy time, not live)
- Share the addon ID for reference
- If provisioning a database for an app that runs migrations on startup, mention that the first deploy after provisioning will run them

**DON'T:**
- Skip the redeploy step — the service won't have the connection string without it
- Leave the human wondering why you're redeploying ("redeploying..." with no context)
- Provision an addon and then forget to redeploy — this is the #1 addon mistake

## Database Introspection

Once an addon is `available`, you can inspect its contents via the data API.

**List tables (Postgres):**

```bash
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://api.buildwithlocus.com/v1/addons/$ADDON_ID/data/tables"
```

Response:
```json
{
  "tables": [
    { "name": "users", "schema": "public", "rowCount": 150, "sizeBytes": 32768, "sizeFormatted": "32.0 KB" },
    { "name": "orders", "schema": "public", "rowCount": 5200, "sizeBytes": 524288, "sizeFormatted": "512.0 KB" }
  ]
}
```

**Describe table schema:**

```bash
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://api.buildwithlocus.com/v1/addons/$ADDON_ID/data/tables/users"
```

**Query rows (paginated):**

```bash
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://api.buildwithlocus.com/v1/addons/$ADDON_ID/data/tables/users/rows?limit=50&offset=0&orderBy=id&orderDir=asc"
```

**Execute read-only SQL (dashboard/inspection only):**

```bash
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"sql": "SELECT count(*) FROM users WHERE active = $1", "params": [true]}' \
  "https://api.buildwithlocus.com/v1/addons/$ADDON_ID/data/query"
```

**List Redis keys:**

```bash
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://api.buildwithlocus.com/v1/addons/$ADDON_ID/data/keys?pattern=user:*&count=100"
```

**Get Redis key value:**

```bash
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://api.buildwithlocus.com/v1/addons/$ADDON_ID/data/keys/session:abc123"
```

**Redis memory info:**

```bash
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://api.buildwithlocus.com/v1/addons/$ADDON_ID/data/memory"
```

## Database Execution (Agent API)

Agents can execute write operations against addon databases. These endpoints allow SQL execution, Redis commands, and transactional migrations.

**Execute SQL (allows writes):**

```bash
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"sql": "INSERT INTO users (name, email) VALUES ($1, $2)", "params": ["Alice", "alice@example.com"]}' \
  "https://api.buildwithlocus.com/v1/addons/$ADDON_ID/execute/sql"
```

**Run a migration (wrapped in transaction, auto-rollback on error):**

```bash
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"sql": "CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT UNIQUE NOT NULL, created_at TIMESTAMP DEFAULT NOW())"}' \
  "https://api.buildwithlocus.com/v1/addons/$ADDON_ID/execute/migrate"
```

**Execute Redis command:**

```bash
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"command": "SET", "args": ["mykey", "myvalue"]}' \
  "https://api.buildwithlocus.com/v1/addons/$ADDON_ID/execute/redis"
```

**Safety:** Dangerous operations are blocked to protect your data.

Blocked SQL patterns: `DROP DATABASE`, `DROP TABLE`, `DROP SCHEMA`, `DROP USER`, `DROP ROLE`, `TRUNCATE`, `GRANT`, `REVOKE`, `CREATE USER`, `CREATE ROLE`, `ALTER USER`, `ALTER ROLE`, `ALTER TABLE ... DROP` (catches `DROP COLUMN`, `DROP CONSTRAINT`, etc.).

Blocked Redis commands: `FLUSHALL`, `FLUSHDB`, `CONFIG`, `SHUTDOWN`, `DEBUG`, `SLAVEOF`, `REPLICAOF`, `CLUSTER`, `SAVE`, `BGSAVE`, `BGREWRITEAOF`.

**Redis shared isolation:** Each Redis addon gets its own database number within a shared Redis instance. Use key prefixes (e.g., `myapp:sessions:`) to avoid collisions if multiple services share the same Redis addon.

## Common Agent Database Workflows

**Create tables + seed data:**

```bash
# 1. Run migration to create schema
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"sql": "CREATE TABLE IF NOT EXISTS posts (id SERIAL PRIMARY KEY, title TEXT NOT NULL, body TEXT, author_id INT REFERENCES users(id), created_at TIMESTAMP DEFAULT NOW())"}' \
  "https://api.buildwithlocus.com/v1/addons/$ADDON_ID/execute/migrate"

# 2. Insert seed data
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"sql": "INSERT INTO posts (title, body, author_id) VALUES ($1, $2, $3)", "params": ["Hello World", "First post", 1]}' \
  "https://api.buildwithlocus.com/v1/addons/$ADDON_ID/execute/sql"

# 3. Verify
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://api.buildwithlocus.com/v1/addons/$ADDON_ID/data/tables/posts/rows"
```

**Multi-statement migration:**

```bash
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"sql": "ALTER TABLE users ADD COLUMN IF NOT EXISTS role TEXT DEFAULT '\''user'\''; CREATE INDEX IF NOT EXISTS idx_users_role ON users(role);"}' \
  "https://api.buildwithlocus.com/v1/addons/$ADDON_ID/execute/migrate"
```
