database-design
Skill2 starsChanged 5 months agoMITView on GitHub
How to design a correct, safe, and scalable database schema — including migrations, indexes, foreign keys, subscription/billing tables, usage tracking, and plan limit enforcement. Use this skill whenever the user is designing a database, adding tables or columns, building a subscription/billing system, adding pro vs free plan features, building usage tracking, or asking about database structure. Also trigger for phrases like "how should I set up the database", "how does Stripe connect to my DB", "how do I track usage",…
- Reads credentials
- Sends data out
58 other projects use a version of this.
Adopted by
Most recently taken up by
- chaunsin/agent-skillstheir own version, first seen Nov 8, 2026
- kinhluan/skillstheir own version, first seen Nov 8, 2026
- ishandutta2007/Awesome-Claude-Skillstheir own version, first seen Nov 8, 2026
- mahdy-gribkov/arcanatheir own version, first seen Nov 8, 2026
- KbWen/agentic-ostheir own version, first seen Nov 7, 2026
- its-ash/skillstheir own version, first seen Nov 1, 2026
Used with
Projects with it also use intelligent-routing, bash-linux and 10 more.
Other files people use alongside this one.
Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
SKB3002Skillintelligent-routing
Automatic agent selection and intelligent task routing. Analyzes user requests and automatically selects the best specialist agent(s) without requiring explicit user mentions.
Also in 6 projects that use this
xenitV1Skillbash-linux
Bash/Linux terminal patterns. Critical commands, piping, error handling, scripting. Use when working on macOS or Linux systems.
Also in 7 projects that use this
xenitV1Skillred-team-tactics
Red team tactics principles based on MITRE ATT&CK. Attack phases, detection evasion, reporting.
Also in 7 projects that use this
pepebot-spaceSkillllm-application-dev
Building applications with Large Language Models - prompt engineering, RAG patterns, and LLM integration. Use for AI-powered features, chatbots, or LLM-based automation.
Also in 4 projects that use this
- +8 more from xenitV1
Other versions58 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 58
- vudovn/ag-kit★ 8.2ksame name
- vudovn/antigravity-kit★ 8.2ksame name
- CloudAI-X/claude-workflow★ 1.4ksame name
- CloudAI-X/claude-workflow-v2★ 1.4ksame name
- MoizIbnYousaf/Ai-Agent-Skills★ 1.1ksame name
- skillcreatorai/Ai-Agent-Skills★ 1.1ksame name
- MoizIbnYousaf/ai-agent-skills★ 1.1ksame name
- KbWen/agentic-os★ 229same name
- kid-sid/claude-spellbook★ 190same name
- xenitV1/Antigravity-Workflows★ 130same name
48 more not listed
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository9 other files it gives its agents.
The file
Show its rules and text
We split this skill into the rules it gives an agent
21 rules. 0 are common, 17 are distinctive, and 4 are very specific to this project.
0
Common
Most of the 55 edited versions we read keep these.
17
Distinctive
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 3
Read this before writing any schema.
- Distinctive: line 14
Every change to the DB goes through a migration file.
- Distinctive: line 21
CREATE INDEX idx_projects_user_id ON projects(user_id).
- Distinctive: line 24
CREATE INDEX idx_users_email ON users(email).
- Distinctive: line 25
CREATE INDEX idx_subscriptions_stripe_customer ON subscriptions(stripe_customer_id).
- Distinctive: line 26
CREATE INDEX idx_usage_user_period ON usage(user_id, period).
- Distinctive: line 64
Never store plan or is_pro on the users table.
- Distinctive: line 78
CREATE INDEX idx_subscriptions_user_id ON subscriptions(user_id).
- Distinctive: line 80
CREATE INDEX idx_subscriptions_stripe_sub ON subscriptions(stripe_sub_id).
- Distinctive: line 82
When limits are in code, changing them requires a deployment.
- Distinctive: line 108
CREATE INDEX idx_usage_user_feature ON usage(user_id, feature).
- Distinctive: line 133
Always check status, not plan name.
- Very specific: line 140
Every limit must be enforced in backend middleware: javascript// middleware/checkLimits.js function checkLimit(feature) { return async …
- Very specific: line 249
Always check subscription status (active/trialing/past_due) not just plan name.
- Very specific: line 250
All feature limits enforced in backend middleware, not frontend.
- Very specific: line 251
Verify Stripe webhook signatures with raw body buffer — raw body must come before express.json().
- Distinctive: line 255
Never trust plan name or price from the frontend — map plan names to Stripe price IDs server-side only.
- Distinctive: line 256
Use parameterized queries everywhere — never concatenate into SQL strings.
- Distinctive: line 257
Add indexes on all foreign keys and columns used in WHERE or ORDER BY clauses.
- Distinctive: line 258
Use migrations for all schema changes — never run raw SQL manually in production.
- Distinctive: line 261
Before writing any table, answer these:
“Common” is measured: kept by at least 6 in 10 of the 55 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
How to design a correct, safe, and scalable database schema — including migrations, indexes, foreign keys, subscription/billing tables, usage tracking, and plan limit enforcement. Use this skill whenever the user is designing a database, adding tables or columns, building a subscription/billing system, adding pro vs free plan features, building usage tracking, or asking about database structure. Also trigger for phrases like "how should I set up the database", "how does Stripe connect to my DB", "how do I track usage", "pro and free plan in database", "database is getting messy", "how do I add a column safely", "migrations", or any time Cursor is about to write CREATE TABLE or ALTER TABLE commands. This skill prevents the most common database blunders that break apps in production.Database Design Skill The Most Common Database Blunders AI Makes Read this before writing any schema. These mistakes look fine at first and destroy apps later.
Blunder 1: No Migrations — Running Raw SQL Manually What AI does: Tells you to run CREATE TABLE users (...) directly in your database console. Why it breaks: Three months later you add a column with ALTER TABLE manually. Now your local DB, production DB, and team member's DB are different shapes. Nobody knows what the "real" schema is. Bugs appear only in production. What to do instead: Use migrations — versioned files that describe every schema change in order. bash# Prisma (recommended for Node.js) npx prisma migrate dev --name add_users_table
Knex
npx knex migrate:make add_users_table Every change to the DB goes through a migration file. You run them in sequence. Every environment stays identical.
Blunder 2: No Indexes on Queried Columns What AI does: Creates tables with columns but never adds indexes. Why it breaks: Works fine with 100 rows. At 100,000 rows, every search query scans the entire table. The app slows to a crawl. What to do: Add indexes on every column you filter or sort by. sql-- Always index foreign keys CREATE INDEX idx_projects_user_id ON projects(user_id);
-- Index columns you search or sort by CREATE INDEX idx_users_email ON users(email); CREATE INDEX idx_subscriptions_stripe_customer ON subscriptions(stripe_customer_id); CREATE INDEX idx_usage_user_period ON usage(user_id, period); Rule of thumb: if you write WHERE column = ? or ORDER BY column, that column needs an index.
Blunder 3: Relationships Without Foreign Key Constraints What AI does: Creates a user_id column in a table but doesn't enforce it as an actual foreign key. Why it breaks: You delete a user, all their projects stay in the DB pointing at a user that no longer exists. Orphaned data. The database has no integrity. What to do: sql-- Always declare foreign keys with ON DELETE behavior user_id UUID REFERENCES users(id) ON DELETE CASCADE -- CASCADE: when user is deleted, their data is deleted too
-- Or use RESTRICT to prevent accidental deletion user_id UUID REFERENCES users(id) ON DELETE RESTRICT -- RESTRICT: can't delete user if they have projects
Blunder 4: Storing Sensitive Data Incorrectly What AI does: Stores plain text passwords, full API keys, sensitive data unencrypted. Rules:
Passwords → hashed with bcrypt, never plain text API keys → stored in .env, never in the DB Sensitive user data → encrypted at rest if required by compliance Stripe keys → never in the DB, only in environment variables
The Correct Schema for Users and Subscriptions Users Table sqlCREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, -- bcrypt hash, NEVER plain text created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() );
CREATE INDEX idx_users_email ON users(email); Subscriptions Table — SEPARATE from users, always The most important rule: Plan is NOT a property of a user. It's a property of a subscription that can expire, get cancelled, go past due, be refunded. Never store plan or is_pro on the users table. A subscription is its own entity. sqlCREATE TABLE subscriptions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, plan VARCHAR(50) NOT NULL, -- 'free', 'pro', 'enterprise' status VARCHAR(50) NOT NULL, -- 'active', 'cancelled', 'past_due', 'trialing' stripe_customer_id VARCHAR(255) UNIQUE, stripe_sub_id VARCHAR(255) UNIQUE, current_period_end TIMESTAMP, -- when billing period ends cancel_at_period_end BOOLEAN DEFAULT FALSE, -- cancelled but still has access until period end created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() );
CREATE INDEX idx_subscriptions_user_id ON subscriptions(user_id); CREATE INDEX idx_subscriptions_stripe_customer ON subscriptions(stripe_customer_id); CREATE INDEX idx_subscriptions_stripe_sub ON subscriptions(stripe_sub_id); Plan Limits Table — Don't Hardcode Limits in Code When limits are in code, changing them requires a deployment. In the DB, you update a row. sqlCREATE TABLE plan_limits ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), plan VARCHAR(50) NOT NULL, -- 'free', 'pro' feature VARCHAR(100) NOT NULL, -- 'projects', 'exports', 'ai_calls' limit_value INTEGER NOT NULL -- -1 means unlimited );
-- Seed data INSERT INTO plan_limits (plan, feature, limit_value) VALUES ('free', 'projects', 3), ('free', 'exports', 0), ('free', 'ai_calls', 10), ('pro', 'projects', -1), ('pro', 'exports', -1), ('pro', 'ai_calls', 500); Usage Tracking Table sqlCREATE TABLE usage ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, feature VARCHAR(100) NOT NULL, used_at TIMESTAMP DEFAULT NOW(), period VARCHAR(7) NOT NULL -- '2024-01' billing period, YYYY-MM format );
CREATE INDEX idx_usage_user_period ON usage(user_id, period); CREATE INDEX idx_usage_user_feature ON usage(user_id, feature); Processed Webhooks Table — Idempotency Stripe sends webhooks multiple times if your server is slow. Without this table, you process the same event twice — user gets two upgrades, or double-charged. sqlCREATE TABLE processed_webhooks ( stripe_event_id VARCHAR(255) PRIMARY KEY, processed_at TIMESTAMP DEFAULT NOW() );
The Correct Subscription Access Check What AI does (wrong): javascriptif (user.plan === 'pro') { allowAccess() } A user can be on the pro plan but their payment failed. Status is past_due. They should NOT have access. The correct check: javascriptfunction hasProAccess(subscription) { if (!subscription) return false
const activeStatuses = ['active', 'trialing'] const withinPeriod = new Date(subscription.current_period_end) > new Date()
return ( activeStatuses.includes(subscription.status) || // Cancelled but paid through end of period — still has access (subscription.cancel_at_period_end && withinPeriod) ) } Always check status, not plan name.
The Correct Feature Limit Enforcement
Frontend-only enforcement is not enforcement. Attackers bypass the frontend and hit your API directly:
bashcurl -X POST https://yourapp.com/api/export
-H "Authorization: Bearer FREE_USER_TOKEN"
Backend exports anyway if there's no server-side check
Every limit must be enforced in backend middleware: javascript// middleware/checkLimits.js function checkLimit(feature) { return async (req, res, next) => { const sub = await getSubscription(req.user.id) const plan = sub?.plan || 'free' const limit = await getPlanLimit(plan, feature) const used = await getUsageCount(req.user.id, feature, currentPeriod())
if (limit !== -1 && used >= limit) {
return res.status(403).json({
error: `${feature} limit reached`,
limit,
used,
upgrade: true
})
}
next()} }
// Applied at the route level — never buried inside feature functions router.post('/ai/generate', checkLimit('ai_calls'), aiController) router.post('/export', checkLimit('exports'), exportController)
The Correct Webhook Handler Mistake: Not Verifying Webhook Signatures Anyone can send a fake webhook to your server claiming a user paid. Without verification, your backend processes it. Correct Pattern: javascript// integrations/stripe/stripeWebhooks.js const stripe = require('./stripeClient') const { stripeWebhook: webhookSecret } = require('../../config/env') const db = require('../../config/database')
// Route must use raw body — before express.json() in app.js app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), async (req, res) => { let event
// 1. Verify signature
try {
event = stripe.webhooks.constructEvent(
req.body,
req.headers['stripe-signature'],
webhookSecret
)
} catch {
return res.status(400).json({ error: 'Invalid signature' })
}
// 2. Idempotency check — don't process duplicates
const already = await db.query(
'SELECT stripe_event_id FROM processed_webhooks WHERE stripe_event_id = $1',
[event.id]
)
if (already.rows.length > 0) return res.json({ received: true })
// 3. Handle the event
await handleByType(event)
// 4. Mark as processed
await db.query(
'INSERT INTO processed_webhooks (stripe_event_id) VALUES ($1)',
[event.id]
)
res.json({ received: true })} ) Webhook Events You Must Handle javascriptconst handlers = { 'checkout.session.completed': handleNewSubscription, 'customer.subscription.updated': handlePlanChange, 'customer.subscription.deleted': handleCancellation, 'invoice.payment_failed': handlePaymentFailed, // downgrade to free 'invoice.payment_succeeded': handleRenewal, 'customer.subscription.trial_ending': handleTrialEnding } Missing invoice.payment_failed means users whose cards expire keep pro access forever.
Billing Security: Never Trust the Client on Price Wrong (AI's version): javascriptapp.post('/create-checkout', async (req, res) => { const { plan, price } = req.body // hacker changes price to 1 cent const session = await stripe.checkout.sessions.create({ line_items: [{ price_data: { unit_amount: price } }] }) }) Correct — price IDs live on the server only: javascript// Server owns the price mapping const PRICES = { pro_monthly: 'price_stripe_id_from_dashboard', pro_annual: 'price_stripe_id_from_dashboard' }
app.post('/create-checkout', async (req, res) => { const { plan } = req.body const priceId = PRICES[plan] if (!priceId) return res.status(400).json({ error: 'Invalid plan' })
const session = await stripe.checkout.sessions.create({ line_items: [{ price: priceId, quantity: 1 }] }) })
Starter Prompt for Cursor (Billing Features) Paste before any billing-related prompt: Subscriptions live in their own table separate from users — never store plan on the users table. Always check subscription status (active/trialing/past_due) not just plan name. All feature limits enforced in backend middleware, not frontend. Verify Stripe webhook signatures with raw body buffer — raw body must come before express.json(). Handle these webhook events: checkout.session.completed, subscription.updated, subscription.deleted, invoice.payment_failed, invoice.payment_succeeded. Store processed webhook IDs in processed_webhooks table to prevent duplicate processing. Never trust plan name or price from the frontend — map plan names to Stripe price IDs server-side only. Use parameterized queries everywhere — never concatenate into SQL strings. Add indexes on all foreign keys and columns used in WHERE or ORDER BY clauses. Use migrations for all schema changes — never run raw SQL manually in production.
Pre-Schema Checklist Before writing any table, answer these:
Does this table have an index on every foreign key? Does this table have indexes on columns used in WHERE/ORDER BY? Are foreign keys declared with ON DELETE behavior? Is subscription data in its own table, not on users? Are plan limits in the DB, not hardcoded? Are there migrations set up, not raw SQL? Are passwords hashed (bcrypt), never plain text? Is there a processed_webhooks table if using Stripe?
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

