db
Scannednpx machina-cli add skill mrsknetwork/supernova/db --openclawDatabase Engineering
Default Stack (Ask First)
Before applying anything, ask:
"Can I use the Supernova database stack? PostgreSQL 16 via SQLAlchemy 2.0 async for server-side, and RxDB v15 for client-side offline-first apps. Or do you have an existing database setup I should match?"
If a DATABASE_URL is already in .env, or an existing migration history exists, detect and match it.
Progressive Disclosure
- Load
references/postgres-advanced.mdfor complex scenarios: JSONB indexing, full-text search, row-level security, CTEs. - Load
references/rxdb.mdfor offline-first client schema, replication protocol, and conflict resolution setup.
SOP: PostgreSQL + SQLAlchemy 2.0 Async
Step 1 - SQLAlchemy ORM Model Conventions
# models/base.py
from sqlalchemy.orm import DeclarativeBase, MappedColumn, mapped_column
from sqlalchemy import func
from uuid import UUID, uuid4
from datetime import datetime
class Base(DeclarativeBase):
pass
# models/user.py
from sqlalchemy.orm import Mapped
from sqlalchemy import String, DateTime, text
class User(Base):
__tablename__ = "users"
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4, server_default=text("gen_random_uuid()"))
email: Mapped[str] = mapped_column(String(320), unique=True, nullable=False, index=True)
display_name: Mapped[str] = mapped_column(String(100), nullable=False)
hashed_password: Mapped[str] = mapped_column(String(255), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
Model Conventions:
- Every table has
id(UUID PK),created_at,updated_atas standard columns. - Use
Mapped[type]andmapped_column()(SQLAlchemy 2.0 style). Never use the 1.xColumn()style. - FK column naming:
{entity}_id(e.g.,user_id,order_id). - Use
Stringwith explicit max lengths, notText, unless unbounded text is genuinely needed.
Step 2 - Relationships
from sqlalchemy.orm import relationship
class User(Base):
# ...
orders: Mapped[list["Order"]] = relationship("Order", back_populates="user", cascade="all, delete-orphan")
class Order(Base):
# ...
user_id: Mapped[UUID] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
user: Mapped["User"] = relationship("User", back_populates="orders")
Always define ondelete behavior on FKs. CASCADE for child data, SET NULL for optional references, RESTRICT (default) when the parent must not be deleted while children exist.
Step 3 - Alembic Migration Workflow
# 1. Generate migration (always auto-generate, never write migrations manually)
alembic revision --autogenerate -m "add_users_table"
# 2. Review the generated file in alembic/versions/. Always read it before applying.
# Check: are the correct columns added? Is upgrade() reversible by downgrade()?
# 3. Apply to development
alembic upgrade head
# 4. Apply to production (in CI/CD, not manually)
alembic upgrade head
Migration rules:
- Every
upgrade()must have a workingdowngrade(). - Never edit a migration that has been applied in production. Create a new migration instead.
- Migration filenames auto-generate a hash. Add a descriptive suffix:
2024_add_users_table.py.
Step 4 - Index Strategy
Add indexes based on actual query patterns. The default indexes to add:
| Scenario | Index Type | Example |
|---|---|---|
| FK column (always) | B-Tree | index=True on user_id FK |
| Email lookup / unique constraint | B-Tree | unique=True on email |
| Status column with low cardinality | Partial B-Tree | CREATE INDEX ON orders (status) WHERE status = 'pending' |
| Full-text search on title/description | GIN | CREATE INDEX ON articles USING GIN (to_tsvector('english', title)) |
| JSONB field lookup | GIN | CREATE INDEX ON products USING GIN (metadata jsonb_path_ops) |
Never add indexes blindly on every column. Indexes slow down writes. Add them where EXPLAIN ANALYZE shows a sequential scan on a table with >10k rows.
Step 5 - Repository Pattern
# repositories/user_repo.py
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update
from uuid import UUID
class UserRepository:
async def get_by_id(self, db: AsyncSession, user_id: UUID) -> User | None:
result = await db.execute(select(User).where(User.id == user_id))
return result.scalar_one_or_none()
async def get_by_email(self, db: AsyncSession, email: str) -> User | None:
result = await db.execute(select(User).where(User.email == email))
return result.scalar_one_or_none()
async def create(self, db: AsyncSession, user: User) -> User:
db.add(user)
await db.commit()
await db.refresh(user)
return user
All DB queries live exclusively in repository files. Services call repositories. Routers never import AsyncSession or select.
Step 6 - Query Anti-Patterns to Avoid
SELECT *- always specify columns or use the ORM model to fetch only what is needed.- Calling
session.execute()in a loop - this produces N+1 queries. Useselectinload()orjoinedload()for relationships. - Implicit type casting in WHERE clauses (e.g., comparing UUID FK to a string) - will cause full table scans.
- Not wrapping multi-statement operations in a transaction.
SOP: RxDB v15 (Client-Side Offline-First)
Use RxDB when the frontend application needs to work offline, sync data on reconnect, or provide an instant-response local-first experience.
Step 1 - Schema Definition
// db/schemas/user.schema.ts
import { RxJsonSchema } from "rxdb";
export const UserSchema: RxJsonSchema<{ id: string; email: string; displayName: string }> = {
version: 0,
primaryKey: "id",
type: "object",
properties: {
id: { type: "string", maxLength: 36 },
email: { type: "string" },
displayName: { type: "string" },
},
required: ["id", "email", "displayName"],
};
version starts at 0 and increments on schema changes. Breaking schema changes require a migration strategy.
Step 2 - Database Initialization
// db/index.ts
import { createRxDatabase, addRxPlugin } from "rxdb";
import { getRxStorageDexie } from "rxdb/plugins/storage-dexie";
import { RxDBDevModePlugin } from "rxdb/plugins/dev-mode";
if (process.env.NODE_ENV !== "production") addRxPlugin(RxDBDevModePlugin);
export async function createDb() {
const db = await createRxDatabase({ name: "myapp", storage: getRxStorageDexie() });
await db.addCollections({ users: { schema: UserSchema } });
return db;
}
Step 3 - Replication with Backend
import { replicateRxCollection } from "rxdb/plugins/replication";
replicateRxCollection({
collection: db.users,
replicationIdentifier: "user-sync",
pull: {
async handler(checkpointOrNull, batchSize) {
const res = await fetch(`/api/v1/sync/users?checkpoint=${checkpointOrNull}&limit=${batchSize}`);
return res.json(); // { documents: [], checkpoint: ... }
},
},
push: {
async handler(rows) {
await fetch("/api/v1/sync/users", { method: "POST", body: JSON.stringify(rows) });
return [];
},
},
});
The sync endpoint on the FastAPI backend must implement the RxDB checkpoint replication protocol.
Overview
This skill designs relational database schemas, writes optimized SQL, and manages Alembic migrations for PostgreSQL. It also configures RxDB for offline-first client apps and emphasizes confirming the existing stack before applying defaults.
How This Skill Works
It uses SQLAlchemy 2.0 async with DeclarativeBase and mapped_column to model tables and columns, enforcing consistent id, created_at, and updated_at fields. It defines relationships with explicit ondelete behavior and follows an Alembic-driven migration workflow (auto-generated migrations, review, and upgrade in dev and production). It also outlines index strategies driven by actual query patterns.
When to Use It
- Designing new relational schemas and migration plans
- Optimizing SQL queries and indexing for performance
- Managing Alembic migrations in CI/CD
- Configuring RxDB for offline-first client apps and replication
- Verifying and matching the existing database stack before defaults
Quick Start
- Step 1: Define base model with DeclarativeBase and mapped_column
- Step 2: Create relationships with ondelete and back_populates
- Step 3: Generate migrations with alembic revision --autogenerate, review, then apply in dev and prod
Best Practices
- Follow model conventions: id (UUID PK), created_at, updated_at on all tables, using Mapped[type] and mapped_column
- Define ondelete behavior for all FKs (CASCADE, SET NULL, or RESTRICT)
- Always run alembic revision --autogenerate and review the file before applying
- Minimize downtime by testing migrations in a dev environment first
- Add indexes based on actual query patterns, starting with FK columns and unique constraints
Example Use Cases
- Users table with UUID PK, email, and timestamp columns
- User–Order relation using user_id FK with CASCADE on delete
- Alembic workflow: autogenerate, review, upgrade in dev, then prod
- Index FK columns and unique constraints like email
- RxDB client schema setup for offline-first apps with replication