Database persistence
Skill batur/ts-baseline-docs/.agents/skills/database-persistence
TypeScript software architecture baseline with VS Code Agent Skills for AI coding, review, security, testing, API design, and delivery standards.
npx -y skills add batur/ts-baseline-docs --skill database-persistenceAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Apply the TypeScript database and persistence architecture standard. Use when adding or reviewing PostgreSQL, Supabase, Drizzle, Prisma, MongoDB, Firebase/Firestore, repositories, migrations, transactions, tenant-scoped queries, serializers, IDs, timestamps, soft delete, raw SQL, indexes, constraints, audit logs, or persistence tests.
SKILL.md
14.1 KB, as published. Nobody here has run it
Database / Persistence Skill
When to use this skill
Use this skill when a task touches database or persistence work:
- Tables, collections, documents, migrations, indexes, constraints, seeds, or schema design.
- PostgreSQL, Supabase, Drizzle, Prisma, MongoDB, Firebase, or Firestore.
- Repositories, query modules, mappers, serializers, transactions, tenant-scoped data access, or raw SQL.
- API data persistence, cursor pagination, audit logs, soft delete, or outbox-like workflows.
- AI review of persistence safety, architecture boundaries, migrations, tenant boundaries, and raw SQL.
Goal
Keep domain/application logic independent from database and provider details while making persistence explicit, safe, testable, and production-ready.
Technology can vary by project. The persistence boundary rules do not.
Default profile
Unless an accepted project decision says otherwise, use this priority:
- PostgreSQL.
- Supabase as the default BaaS profile.
- MongoDB only when a document-first model is justified.
- Firebase / Firestore when realtime, offline, or client-first development is central.
For PostgreSQL/Supabase backend work, default to:
PostgreSQL or Supabase Postgres + Drizzle ORM + versioned migrations
Prisma is an accepted alternative profile, especially for generated-client DX, broader database support, or MongoDB support.
The persistence boundary is more important than the selected ORM.
Core rules
- PostgreSQL is the default relational database.
- Supabase is the default Backend-as-a-Service profile.
- Supabase Postgres is treated as PostgreSQL.
- Drizzle is the default ORM for PostgreSQL/Supabase backend boilerplates.
- Prisma is an accepted alternative, not the default.
- MongoDB and Firebase/Firestore are supported profiles only when justified.
- Domain/application code must not directly import Drizzle, Prisma, Supabase JS, Firebase SDK, MongoDB driver, or provider-specific persistence SDKs.
- Use repository/query modules when business logic depends on persistence.
- Use serializers to produce API DTOs. Never return DB rows/documents directly from APIs.
- Use versioned migrations where supported.
- Do not use production schema push as the deployment mechanism.
- Use explicit transactions for related writes.
- Tenant-scoped resources must include tenant/organization filters in queries.
- Raw SQL requires written justification, parameterization, tenant-safety, explicit review, and tests when critical.
Profile rules
PostgreSQL / Supabase
- Use Drizzle by default.
- Use versioned migrations.
- Define constraints, indexes, timestamps, and tenant keys explicitly.
- Use
snake_casetable/column names. - Use
camelCaseTypeScript fields. - Keep Supabase service-role keys server-only.
- Client-side Supabase access must rely on reviewed RLS policies.
- Server-side privileged operations must go through use-cases/adapters.
- Supabase JS must not leak into domain/application logic.
Prisma
Use Prisma when the project explicitly values generated-client DX, fast CRUD-heavy development, broader database support, or MongoDB support.
Rules:
- Keep Prisma Client behind repositories/query modules.
- Do not use generated Prisma models as API DTOs by default.
- Use migrations for supported relational databases.
- Be careful with Supabase RLS and privileged server access.
MongoDB
Use MongoDB only when the data model is naturally document-oriented.
Good fit: nested aggregates, flexible schema as a real advantage, low join/reporting needs, content/event/log/document-heavy data.
Poor fit: strong relational integrity, complex joins/reporting, billing/permissions/tenant relations, or transaction-heavy workflows.
Rules:
- Validate boundaries with Zod.
- Do not accept raw Mongo query objects from clients.
- Define indexes intentionally.
- Review multi-document transaction needs.
- Keep driver/ODM code behind adapters/repositories.
Firebase / Firestore
Use Firebase/Firestore when realtime, offline, or client-first development is central.
Rules:
- Firestore Security Rules are part of application security.
- Test Security Rules when clients access Firestore directly.
- Keep Firebase Admin SDK behind server adapters.
- Document intentional denormalization and query/index trade-offs.
Folder placement
Simple backend shape:
src/
shared/database/
database.ts
schema/*.table.ts
modules/users/
user.repository.ts
drizzle-user.repository.ts
user.mapper.ts
create-user.use-case.ts
user.serializer.ts
drizzle/migrations/
Complex component shape, only when complexity requires it:
src/modules/users/
application/user.repository.ts
application/create-user.use-case.ts
domain/user.ts
infrastructure/drizzle-user.repository.ts
infrastructure/user.mapper.ts
presentation/user.serializer.ts
Do not create empty layered folders just to look architectural.
Naming, IDs, and timestamps
SQL database naming:
- Tables/columns:
snake_case. - TypeScript models/DTOs:
camelCase. - Mapping happens at repository/mapper/serializer boundaries.
Public IDs:
- Do not expose auto-increment integers by default.
- Prefer string IDs, ideally resource-prefixed:
usr_...,org_...,prj_...,api_key_.... - UUID is acceptable.
Timestamps:
- Mutable relational tables should include
created_atandupdated_at. - Soft-deletable tables include
deleted_at. - Store timestamps in UTC.
- API responses return ISO 8601 strings.
Soft delete rules
Soft delete is opt-in.
Use it for recoverable user-facing data, compliance/audit-sensitive records, or business data where history matters.
Avoid it for temporary tokens, cache tables, ephemeral jobs, and many simple join tables.
Default queries must exclude soft-deleted records.
Repository and query module rules
Use repositories when:
- Business flow depends on DB operations.
- Transaction boundaries exist.
- Tenant boundaries must be enforced.
- The dependency should be fakeable in use-case tests.
- Database/provider details should stay outside application/domain logic.
Use query modules for simple read-only/reporting queries when a repository abstraction adds ceremony without value.
Repository interfaces belong on the application/domain side when abstraction is needed. Implementations belong in infrastructure/persistence.
Mapping and serializer rules
Never return DB rows/documents directly from API handlers.
Expected flow:
DB row/document → mapper/repository → domain/application model → serializer → API DTO
Example row mapping:
function mapUserRow(row: UserRow): User {
return {
id: row.id,
email: row.email,
displayName: row.display_name,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
}
Example serializer:
function serializeUser(user: User): UserResponse {
return {
id: user.id,
email: user.email,
displayName: user.displayName,
createdAt: user.createdAt.toISOString(),
};
}
Transaction rules
Use explicit transactions for related writes, such as:
- User creation + organization membership.
- Payment + subscription update.
- Order + inventory update.
- State change + outbox/event write.
- Critical mutation + audit log.
The transaction boundary belongs in the use-case/application layer. Repositories may accept a transaction context.
await DATABASE.transaction(async (tx) => {
await USER_REPOSITORY.create(input, tx);
await AUDIT_LOG_REPOSITORY.create(event, tx);
});
Tenant-safe query rules
Tenant-scoped access must include organizationId or tenantId.
Prefer:
await PROJECT_REPOSITORY.findByIdForOrganization(projectId, organizationId);
Avoid for tenant-scoped resources:
await PROJECT_REPOSITORY.findById(projectId);
Admin/internal access must be explicit:
await PROJECT_REPOSITORY.findByIdForAdmin(projectId);
Cross-tenant access should usually behave as not found to avoid resource enumeration.
Constraints, indexes, and pagination
Application validation is not enough for data integrity.
Use DB constraints for critical invariants where practical:
- Unique email, slug, or
(organization_id, slug). - Foreign keys.
- Not-null constraints.
- Check constraints where appropriate.
Add indexes intentionally for common filters/sorts:
organization_idcreated_atstatus- foreign keys
- unique natural keys
Cursor pagination is preferred for public collections. Use deterministic ordering, commonly created_at DESC, id DESC. Offset pagination is acceptable for small internal/admin lists.
Raw SQL review gate
Raw SQL is allowed only when ORM/query builder is insufficient or makes the query less clear/safe.
Every raw SQL block must include written justification covering:
- Why ORM/query builder is not enough.
- Parameterization of all user input.
- Tenant/organization boundary.
- Index/performance implications.
- Transaction needs.
- Test coverage.
- Reviewer expectation.
Required pattern:
/**
* Raw SQL justification:
* - Reason: Drizzle query builder cannot express this recursive CTE clearly.
* - Safety: All user inputs are parameterized.
* - Tenant boundary: organization_id is included in WHERE clause.
* - Review: Requires persistence/security review.
*/
const rows = await db.execute(sql`
SELECT * FROM projects
WHERE id = ${projectId}
AND organization_id = ${organizationId}
`);
Never concatenate user input into query strings.
// Forbidden
sql.raw(`SELECT * FROM users WHERE email = '${email}'`);
Supabase, Firebase, and client access rules
Supabase:
- Service role key is server-only.
- Client-side access must rely on reviewed RLS.
- RLS policies must be reviewed when tables or access patterns change.
- Server-side privileged operations go through adapters/use-cases.
Firebase/Firestore:
- Security Rules are part of the security model.
- Test Security Rules for client-side direct access.
- Keep Admin SDK behind server adapters.
- Do not treat frontend checks as database security.
Audit and outbox rules
Application logs and audit logs are different.
Use audit logs for sensitive business/security actions:
- Role/permission changed.
- Billing plan changed.
- API key created/revoked.
- Organization member removed.
- Security-sensitive admin action.
Outbox pattern is optional advanced profile. Use it when reliable event/job/webhook publishing must be tied to a DB write.
Seeding rules
- Dev seed is allowed.
- Test seed must be deterministic.
- Production seed requires explicit/manual review.
- Seeds must not accidentally modify production data.
Coding workflow
When adding persistence code:
- Identify the persistence profile.
- Check ADR/docs before choosing provider or ORM.
- Keep SDK/ORM imports out of domain/application logic.
- Add/update schema and migration when persistence shape changes.
- Put business-related data access behind repository/query modules.
- Add tenant/org filters for tenant-scoped data.
- Use transactions for related writes.
- Add constraints/indexes for important invariants and access patterns.
- Map DB/document models to domain/application models.
- Serialize API responses explicitly.
- Add raw SQL justification and tests if raw SQL is used.
- Update docs, ADRs, OpenAPI, and tests when relevant.
Review checklist
Before accepting persistence changes, verify:
- PostgreSQL/Supabase/Drizzle default is followed unless another profile is justified.
- Prisma/MongoDB/Firebase usage has an explicit project reason.
- Domain/application code does not import ORM/SDK/provider details.
- Schema changes include migrations where applicable.
- Production schema push is not assumed.
- SQL names are
snake_case; TypeScript names arecamelCase. - Public IDs are not auto-increment integers.
- Mutable tables have timestamps.
- Soft delete is opt-in and default queries exclude deleted rows.
- Related writes use transactions.
- Tenant-scoped queries include tenant/org filters.
- DB rows/documents are not returned directly from APIs.
- Serializers produce response DTOs.
- Critical invariants have DB constraints where practical.
- Indexes exist for important filters/sorts.
- Cursor pagination uses deterministic ordering.
- Raw SQL has justification, parameterization, tenant filtering, explicit review, and tests.
- Supabase service role key is server-only.
- RLS/Security Rules are reviewed when applicable.
- Persistence tests cover critical behavior.
Required tests
Add or update tests for:
- Repository/query behavior when persistence logic is important.
- Tenant boundary enforcement.
- Transactional workflows.
- Soft delete default exclusion.
- Unique/conflict handling.
- Cursor pagination ordering.
- Raw SQL behavior and security-relevant filters.
- Provider/DB error mapping when relevant.
- Supabase RLS or Firestore Security Rules when direct client access exists.
Anti-patterns
Avoid:
- Returning DB rows/documents directly from API handlers.
- Importing persistence SDKs inside domain/application logic.
- Generic tenant-scoped
findByIdwithout tenant filter. - Production schema push.
- Exposed auto-increment public IDs.
- Implicit multi-write operations without transactions.
- Raw SQL without justification.
- Raw SQL with string-concatenated user input.
- Client-provided Mongo query objects.
- Frontend checks as database security.
- Repositories for every trivial read.
- Layered folders before complexity exists.
Done criteria
A persistence change is done when:
- It follows the selected persistence profile.
- The persistence boundary is respected.
- Schema/migration changes are complete where applicable.
- Tenant, auth, transaction, and raw SQL safety are handled.
- API DTOs are produced through serializers.
- Critical tests are added or updated.
- Docs, ADRs, and OpenAPI are updated when the change affects contracts or architecture.