Apply when writing or modifying database schema, queries, migrations, transactions, or tenant-scoped persistence.
Scanned 9/3/2026
Install to Claude Code
npx -y skills add stella/stella --skill conventions-db --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Conventions Db?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/stella-conventions-db-stella)More formats (shields.io, HTML) on the badges page.
---
name: conventions-db
description: 'Apply when writing or modifying database schema, queries, migrations, transactions, or tenant-scoped persistence.'
---
# Database Conventions
Apply when writing or modifying database schema, queries, migrations,
transactions, or tenant-scoped persistence.
## Schema
- Schema lives under `apps/api/src/db/schema/`; read the owning slice and
related foreign keys before editing.
- For closed persisted domain values, define one named `as const` value list
and pass it to Drizzle with `text({ enum: VALUES })`. Add a database `CHECK`
when invalid values could compromise lifecycle, authorization, audit, or
workflow invariants; Drizzle's enum option is compile-time-only.
- Reserve `.$type<T>()` for branded or structured types. Use a native
PostgreSQL enum only when the value set is genuinely permanent.
- Use cascade deletes for workspace-owned dependants and restrict deletes for
shared file references. Trace the full FK graph before multi-resource
deletion.
- Timestamp columns use the `timestamptz` helper from
`apps/api/src/db/columns.ts`. Never introduce a naive PostgreSQL timestamp or
`::timestamp` cast without explicitly anchoring its time zone.
- Add indexes for columns used in `WHERE`, `ORDER BY`, or `JOIN`; lead
composite indexes with the tenant-scoping columns. Treat changes to large
tables as lock-sensitive.
## Migrations
- `bun --filter @stll/api db:migrate` is the shipped migration path used by CI
and deployment. `db:push` is a local declarative schema-diff tool; it does
not replace committed migrations and must not be described as the deployment
path.
- Schema changes remain additive across a rollout: add, deploy compatible
reads/writes, backfill in bounded batches, switch, then remove the old shape
in a later release.
- Migrations run before new API tasks finish rolling out. Old tasks must remain
compatible with the migrated schema, and a failed rollout must have a safe
forward-fix path.
- Keep irreversible schema operations out of the same release as risky
application changes. Destructive, bulk-backfill, and access-control SQL
requires a statement-scoped acknowledgement enforced by
`scripts/check-migration-safety.ts`, placed in the comment block directly
above the statement:
`-- stella-migration-safety: reviewed <rule-id> - <why this is safe>`.
An acknowledgement that clears nothing is an error. Every migration sets
`lock_timeout` and `statement_timeout` first.
- For large live tables, follow the repository's guarded concurrent-index
protocol: either split and reopen the migrator transaction exactly as
enforced by `migration-concurrent-index.test.ts`, or put repairable work in
`online-migrations.ts`. Keep long backfills outside schema migrations and
checkpoint them durably.
- Validate migration history two ways: apply every committed migration to a
fresh database, then confirm
`bun --filter @stll/api db:push -- --explain` reports no schema drift. Do not
repair drift by resetting a shared database.
## Tenant Scope and Queries
- Workspace data uses the authorized `scopedDb` supplied by safe handlers so
PostgreSQL RLS and query-level scope reinforce each other. Raw/root database
access needs a demonstrated system-level reason and a deny-by-default RLS
posture.
- Ownership IDs come from server-validated context, never request bodies. Keep
tenant predicates in the database query even when a preceding authorization
check exists.
- Prefer Drizzle's relational query API for ordinary relation reads. Use
SQL-like syntax for cross-table filtering, aggregation, locking, unions, or
mutations where it expresses the invariant more directly.
- Every list query uses a bounded `limit` and cursor and returns the standard
`Page<T>` envelope from `apps/api/src/lib/pagination.ts`. Offset pagination,
`totalCount`, and unbounded `findMany` require explicit justification.
- Do not filter unindexed JSONB in request paths. Fetch through indexed tenant
columns, then narrow structured content with a type guard rather than a cast.
- Batch relation reads and writes. Never issue a query per item when a join,
relation preload, `IN` query, or bulk mutation can express the same work.
## Concurrency and Transactions
- Keep transactions short; perform S3, network, conversion, and other external
I/O outside them.
- Close every read-decide-write race. Lock the decisive row with `SELECT ...
FOR UPDATE`, or encode the expected state/version in the mutation `WHERE`
clause and check the affected-row count.
- Make retries converge. Stable identities, unique constraints, conditional
transitions, and idempotency keys are stronger than read-before-insert
checks.
- Preserve lock order across call sites. When multiple resources must be
locked, define and reuse a deterministic ordering to avoid deadlocks.
## Verification
Test behavior that schema inference cannot prove: cross-tenant denial,
concurrent transitions, replay/idempotence, migration parity, destructive
delete ordering, and cursor stability under inserts. Prefer invariant or
integration tests over mocked query-shape tests.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!