Back to Skills

vanilla-rails-data-modeling

verified

Use when designing database schema, writing migrations, or making data storage decisions - enforces UUIDs, account_id multi-tenancy, state-as-records, no foreign keys, and proper index patterns

View on GitHub

Marketplace

zemptime-marketplace

ZempTime/zemptime-marketplace

Plugin

vanilla-rails

Repository

ZempTime/zemptime-marketplace
1stars

vanilla-rails/skills/data-modeling/SKILL.md

Last Verified

January 20, 2026

Install Skill

Select agents to install to:

Scope:
npx add-skill https://github.com/ZempTime/zemptime-marketplace/blob/main/vanilla-rails/skills/data-modeling/SKILL.md -a claude-code --skill vanilla-rails-data-modeling

Installation paths:

Claude
.claude/skills/vanilla-rails-data-modeling/
Powered by add-skill CLI

Instructions

# Vanilla Rails Data Modeling

Database schema conventions following production 37signals patterns. Design for multi-tenancy, auditability, and operational flexibility.

## UUID Primary Keys

**All tables use UUIDs** - no auto-incrementing integers.

```ruby
# ❌ BAD - default integer
create_table :cards do |t|
  t.string :title
end

# ✅ GOOD - explicit UUID
create_table :cards, id: :uuid do |t|
  t.string :title
end
```

**UUID format:** UUIDv7 (timestamp-ordered), base36 encoded as 25-character strings.

**Why UUIDs:**
- No ID enumeration attacks
- Merge-safe across environments
- Timestamp ordering preserved (UUIDv7)
- No sequence contention under load

**Fixture considerations:** Fixtures need deterministic UUIDs that sort "older" than runtime records. Use a custom generator based on fixture name hash.

## Multi-Tenancy via account_id

**Every tenant-scoped table has `account_id`** - no exceptions for tables containing user data.

```ruby
create_table :cards, id: :uuid do |t|
  t.uuid :account_id, null: false  # Always present
  t.uuid :board_id, null: false
  t.string :title
  t.timestamps
end
```

**Tables WITHOUT account_id** (global/cross-tenant):
- `identities` - email addresses span accounts
- `sessions` - tied to identity, not account
- `magic_links` - authentication, not tenant data

**Automatic scoping:** Use `Current.account` and ApplicationRecord to scope queries:

```ruby
class ApplicationRecord < ActiveRecord::Base
  def self.default_scope
    if Current.account
      where(account_id: Current.account.id)
    else
      all
    end
  end
end
```

**Common mistake:** Forgetting account_id on join tables:

```ruby
# ❌ BAD - missing account_id
create_table :taggings, id: :uuid do |t|
  t.uuid :card_id, null: false
  t.uuid :tag_id, null: false
end

# ✅ GOOD - includes account_id
create_table :taggings, id: :uuid do |t|
  t.uuid :account_id, null: false
  t.uuid :card_id, null: false
  t.uuid :tag_id, null: false
end
```

## State as Records (NOT Boolean

Validation Details

Front Matter
Required Fields
Valid Name Format
Valid Description
Has Sections
Allowed Tools
Instruction Length:
10736 chars

Issues Found:

  • name_directory_mismatch