/archestra-dev-interactions-migrations
Use BEFORE writing or running any Drizzle migration that touches the `interactions` table (or any other very large, write-hot table). The interactions table is the platform's biggest, append-heavy table — every LLM proxy call writes a row — so a careless migration can take a
$ npx -y skills add archestra-ai/archestra --skill archestra-dev-interactions-migrations --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/archestra-dev-interactions-migrations
Context preview
The summary Claude sees to decide when to auto-load this skill.
Use BEFORE writing or running any Drizzle migration that touches the `interactions` table (or any other very large, write-hot table). The interactions table is the platform's biggest, append-heavy table — every LLM proxy call writes a row — so a careless migration can take a
SKILL.md
archestra-dev-interactions-migrations.SKILL.mdname: archestra-dev-interactions-migrations
description: Use BEFORE writing or running any Drizzle migration that touches the `interactions` table (or any other very large, write-hot table). The interactions table is the platform's biggest, append-heavy table — every LLM proxy call writes a row — so a careless migration can take a write-blocking lock and stall the proxy. Covers which operations are safe vs table-rewriting/lock-taking, the "never rebuild an index in a transactional migration" rule, and a read-only audit procedure against the GKE staging database to size the risk first.
Migrations against the `interactions` table
The `interactions` table is special: it is the largest table in the platform and is on the LLM proxy's hot write path — every proxied LLM call inserts a row. A migration that takes a strong lock on it, even briefly, blocks those inserts, so the proxy cannot record interactions until the migration finishes. On a large table a "quick" `CREATE INDEX` can hold that lock for minutes.
Treat any schema change to `interactions` as production-risk work. The same rules apply to any other very large, write-hot table.
Safe vs risky operations
Safe (fast, metadata-only, no table rewrite in PostgreSQL 11+):
- `ADD COLUMN ... DEFAULT <constant> NOT NULL` — the default is stored as
metadata; existing rows are not rewritten. This is instant regardless of table size. (The billing_mode column was added this way.)
- `ADD COLUMN` nullable, with no default.
- `DROP DEFAULT`, `SET DEFAULT <constant>`, renaming a column.
Risky (rewrites the whole table or takes a write-blocking lock — scales with table size):
- `ADD COLUMN ... DEFAULT <volatile expr>` (e.g. `now()`, `gen_random_uuid()`) —
rewrites every row.
- `ALTER COLUMN ... TYPE ...` — usually rewrites the table.
- `SET NOT NULL` on an existing column — full scan to validate.
- **`CREATE INDEX` / `DROP INDEX` (non-concurrent)** — this is the most common
trap. A plain `CREATE INDEX` takes a `SHARE` lock that blocks writes for the entire build; adding a column to an existing covering index means a `DROP INDEX` + `CREATE INDEX` rebuild.
The index rule
**Never add, drop, or rebuild an index on `interactions` inside a Drizzle migration.** Drizzle runs each migration in a single transaction, and `CREATE INDEX CONCURRENTLY` / `DROP INDEX CONCURRENTLY` cannot run inside a transaction — so the only thing a generated migration can emit is the blocking, non-concurrent form.
Instead:
1. Keep the Drizzle schema's index definition matching what is actually deployed, so `pnpm db:generate` does not emit an index change. If you need a new index for a query, decide whether the query can tolerate a heap fetch instead — for an analytics query (not the hot path) it usually can. 2. If the index is genuinely needed, apply it out of band as an ops step with `CREATE INDEX CONCURRENTLY` (and `DROP INDEX CONCURRENTLY` for the old one) during a maintenance window, then update the schema to match. `CONCURRENTLY` builds without blocking writes, at the cost of a slower build and a second table scan.
The migration linter (`pnpm --dir backend check:migrations`) flags `DROP INDEX` as an error and non-concurrent `CREATE INDEX` as a warning for exactly this reason. If it fires on an `interactions` migration, stop and rework the change — do not just add the `allow-breaking` marker.
Audit the table on staging before you ship
Before merging a migration that touches `interactions`, size the real table on the GKE staging database so you know the blast radius. This is **read-only** — never run the migration DDL by hand against staging or production; migrations deploy through the normal pipeline.
Access is via GCP/GKE IAM (managed separately from this repo), so the commands below grant nothing on their own.
1. Switch kubectl to the GKE staging context:
kubectl config get-contexts -o name | grep archestra-staging
# e.g. gke_<project>_us-central1-a_archestra-staging
kubectl config use-context <that-context>
2. Find the Postgres pod (namespace `archestra`, container `postgresql`):
kubectl get pods -n archestra | grep postgresql # archestra-platform-postgresql-0
3. Open a read-only psql session (use the app credentials already in the pod's environment; do not export secrets):
kubectl exec -it -n archestra archestra-platform-postgresql-0 -c postgresql \
-- bash -lc 'PGPASSWORD="$POSTGRES_PASSWORD" psql -U "$POSTGRES_USER" -d "$POSTGRES_USER"'4. Run the audit queries (all read-only):
-- PostgreSQL version. Metadata-only ADD COLUMN ... DEFAULT needs 11+.
SELECT version();
-- Fast row estimate. NEVER run count(*) on this table — it scans everything.
SELECT reltuples::bigint AS est_rows, relpages
FROM pg_class WHERE relname = 'interactions';
-- Heap / TOAST / index sizes.
SELECT pg_size_pretty(pg_total_relation_size('interactions')) AS total,
pg_size_pretty(pg_relation_size('interactions')) AS heap,
pg_size_pretty(pg_indexes_size('interactions')) AS indexes;
-- Per-index size — a non-concurrent rebuild is at least this expensive.
SELECT indexrelname,
pg_size_pretty(pg_relation_size(indexrelid)) AS size
FROM pg_stat_user_indexes
WHERE relname = 'interactions'
ORDER BY pg_relation_size(indexrelid) DESC;
-- Long-running transactions. A CREATE INDEX waits behind these AND, once it
-- starts, blocks writes until it finishes — so know what's open first.
SELECT pid, now() - xact_start AS xact_age, state, left(query, 80) AS query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL AND pid <> pg_backend_pid()
ORDER BY xact_start
LIMIT 10;5. Read the numbers:
- Metadata-only changes (safe `ADD COLUMN`) are effectively instant no matter
how big the table is — ship them normally.
- A table rewrite or a non-concur
Read more
name: archestra-dev-interactions-migrations description: Use BEFORE writing or running any Drizzle migration that touches the `interactions` table (or any other very large, write-hot table). The interactions table is the platform's biggest, append-heavy table — every LLM proxy call writes a row — so a careless migration can take a write-blocking lock and stall the proxy. Covers which operations are safe vs table-rewriting/lock-taking, the "never rebuild an index in a transactional migration" rule, and a read-only audit procedure against the GKE staging database to size the risk first.
Migrations against the `interactions` table
The `interactions` table is special: it is the largest table in the platform and is on the LLM proxy's hot write path — every proxied LLM call inserts a row. A migration that takes a strong lock on it, even briefly, blocks those inserts, so the proxy cannot record interactions until the migration finishes. On a large table a "quick" `CREATE INDEX` can hold that lock for minutes.
Treat any schema change to `interactions` as production-risk work. The same rules apply to any other very large, write-hot table.
Safe vs risky operations
Safe (fast, metadata-only, no table rewrite in PostgreSQL 11+):
- `ADD COLUMN ... DEFAULT <constant> NOT NULL` — the default is stored as
metadata; existing rows are not rewritten. This is instant regardless of table size. (The billing_mode column was added this way.)
- `ADD COLUMN` nullable, with no default.
- `DROP DEFAULT`, `SET DEFAULT <constant>`, renaming a column.
Risky (rewrites the whole table or takes a write-blocking lock — scales with table size):
- `ADD COLUMN ... DEFAULT <volatile expr>` (e.g. `now()`, `gen_random_uuid()`) —
rewrites every row.
- `ALTER COLUMN ... TYPE ...` — usually rewrites the table.
- `SET NOT NULL` on an existing column — full scan to validate.
- **`CREATE INDEX` / `DROP INDEX` (non-concurrent)** — this is the most common
trap. A plain `CREATE INDEX` takes a `SHARE` lock that blocks writes for the entire build; adding a column to an existing covering index means a `DROP INDEX` + `CREATE INDEX` rebuild.
The index rule
**Never add, drop, or rebuild an index on `interactions` inside a Drizzle migration.** Drizzle runs each migration in a single transaction, and `CREATE INDEX CONCURRENTLY` / `DROP INDEX CONCURRENTLY` cannot run inside a transaction — so the only thing a generated migration can emit is the blocking, non-concurrent form.
Instead:
1. Keep the Drizzle schema's index definition matching what is actually deployed, so `pnpm db:generate` does not emit an index change. If you need a new index for a query, decide whether the query can tolerate a heap fetch instead — for an analytics query (not the hot path) it usually can. 2. If the index is genuinely needed, apply it out of band as an ops step with `CREATE INDEX CONCURRENTLY` (and `DROP INDEX CONCURRENTLY` for the old one) during a maintenance window, then update the schema to match. `CONCURRENTLY` builds without blocking writes, at the cost of a slower build and a second table scan.
The migration linter (`pnpm --dir backend check:migrations`) flags `DROP INDEX` as an error and non-concurrent `CREATE INDEX` as a warning for exactly this reason. If it fires on an `interactions` migration, stop and rework the change — do not just add the `allow-breaking` marker.
Audit the table on staging before you ship
Before merging a migration that touches `interactions`, size the real table on the GKE staging database so you know the blast radius. This is **read-only** — never run the migration DDL by hand against staging or production; migrations deploy through the normal pipeline.
Access is via GCP/GKE IAM (managed separately from this repo), so the commands below grant nothing on their own.
1. Switch kubectl to the GKE staging context:
kubectl config get-contexts -o name | grep archestra-staging # e.g. gke_<project>_us-central1-a_archestra-staging kubectl config use-context <that-context>
2. Find the Postgres pod (namespace `archestra`, container `postgresql`):
kubectl get pods -n archestra | grep postgresql # archestra-platform-postgresql-0
3. Open a read-only psql session (use the app credentials already in the pod's environment; do not export secrets):
kubectl exec -it -n archestra archestra-platform-postgresql-0 -c postgresql \
-- bash -lc 'PGPASSWORD="$POSTGRES_PASSWORD" psql -U "$POSTGRES_USER" -d "$POSTGRES_USER"'4. Run the audit queries (all read-only):
-- PostgreSQL version. Metadata-only ADD COLUMN ... DEFAULT needs 11+.
SELECT version();
-- Fast row estimate. NEVER run count(*) on this table — it scans everything.
SELECT reltuples::bigint AS est_rows, relpages
FROM pg_class WHERE relname = 'interactions';
-- Heap / TOAST / index sizes.
SELECT pg_size_pretty(pg_total_relation_size('interactions')) AS total,
pg_size_pretty(pg_relation_size('interactions')) AS heap,
pg_size_pretty(pg_indexes_size('interactions')) AS indexes;
-- Per-index size — a non-concurrent rebuild is at least this expensive.
SELECT indexrelname,
pg_size_pretty(pg_relation_size(indexrelid)) AS size
FROM pg_stat_user_indexes
WHERE relname = 'interactions'
ORDER BY pg_relation_size(indexrelid) DESC;
-- Long-running transactions. A CREATE INDEX waits behind these AND, once it
-- starts, blocks writes until it finishes — so know what's open first.
SELECT pid, now() - xact_start AS xact_age, state, left(query, 80) AS query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL AND pid <> pg_backend_pid()
ORDER BY xact_start
LIMIT 10;5. Read the numbers:
- Metadata-only changes (safe `ADD COLUMN`) are effectively instant no matter
how big the table is — ship them normally.
- A table rewrite or a non-concur
Enterprise AI Platform with guardrails, MCP registry, gateway & orchestrator
Repo: archestra-ai/archestra
Other skills on archestra.
- /archestra-dev-backend-tests
Use when writing or modifying Archestra backend unit tests (platform/backend/src/**/*.test.ts) — mocking modules, stubbing globals, database fixtures, vitest projects/isolation, or test performance.
Open skill - /archestra-dev-backend
Use when adding or changing Archestra backend routes, models, services, API request/response schemas, endpoint permissions, or OpenAPI/codegen for the generated API client.
Open skill - /archestra-dev-bench-analysis
Map-reduce a finished archestra-bench run into a Tier-1/Tier-2 improvement report using Claude subagents (same analysis as the Rust analyzer, no API key).
Open skill - /archestra-dev-e2e
Use when writing, debugging, or running Archestra Playwright e2e tests, API/UI fixtures, WireMock-backed tests, local/CI e2e setup, or test selectors.
Open skill - /archestra-dev-frontend
Use when modifying Archestra frontend Next.js/React code, UI components, forms, TanStack Query hooks, generated API client usage, frontend copy, or documentation links.
Open skill - /archestra-dev-investigate
Use when investigating Archestra bugs or incidents — staging issues, backend 50x errors, Drizzle failed queries, DB connection pressure, deploy regressions, or Kubernetes/runtime symptoms. Orientation only; defers the process to /investigate.
Open skill

