This application is not multi-tenant
Multi-tenancy is easiest to understand from an application that doesn't have it, because the gap is measurable.
The measurement
tables: 33
with user_id (13):
bookmarks, community_comments, enrollments, experience_points,
identity_authentications, lesson_completions, lesson_notes,
partner_referrals, quiz_attempts, solution_reveals, submissions,
user_badges, user_preferences
without user_id (20):
action_text_rich_texts, active_storage_attachments,
active_storage_blobs, active_storage_variant_records, badges,
categories, challenges, courses, lessons, levels,
partner_api_tokens, partner_course_sponsorships,
partner_handoff_nonces, partner_inquiries, partner_roles,
partners, quiz_options, quiz_questions, quizzes, users
That's db/schema.rb on 2026-10-05. Four of the twenty are Rails infrastructure (Action Text and Active Storage), so the real shape is twenty-nine domain tables: thirteen carrying a user_id, sixteen not.
Why user_id isn't a tenant key
The whole subject rests on this distinction, and the two look identical in a schema.
user_id answers "who owns this row?" A lesson completion belongs to a learner. Another learner's completion is a different row, and both are ordinary data in one shared table.
account_id answers "whose data partition is this?" Two accounts must never see each other's rows. A query that returns one account's data to another is a data breach, not a bug in a feature.
The difference is in what happens when the scope is forgotten.
Forget user_id on a lesson-completion query and a learner sees the wrong progress number, which is embarrassing. Forget account_id on the same query in a multi-tenant application and one customer sees another customer's data, which is an incident with a legal side.
flowchart TB
U["user_id: who owns this row"] --> U1["forgotten: a wrong number on a page"]
A["account_id: whose partition this is"] --> A1["forgotten: one customer sees another's data"]
U1 --> D["same column shape, different consequence"]
A1 --> D
What's missing
Identity::Current has one attribute:
module Identity
class Current < ActiveSupport::CurrentAttributes
attribute :user
end
end
In a multi-tenant application this is where the tenant lives, because it's the one place every layer can read without passing it through every method signature. There's no attribute :account, and nothing to add one for.
The sixteen domain tables without a user_id fall into groups. Nine are the shared catalog (courses, lessons, challenges, quizzes, quiz_questions, quiz_options, categories, levels, badges), and they're correct as they stand: every learner sees the same courses, by design. Four belong to a partner through partner_id (partner_api_tokens, partner_course_sponsorships, partner_handoff_nonces, partner_roles), which is the nearest thing this application has to a tenant key: one partner's API tokens must never be visible to another. The rest are the owners themselves (partners, users) and inbound partner inquiries.
That's the significant fact. A multi-tenant version of this application wouldn't add a tenant key to twenty-nine tables, because the catalog is shared. Lesson 3 is about how a table earns the key or doesn't.
When it would arrive
One sentence changes everything: "a company wants to buy fifty seats and see their team's progress."
That isn't a request for a report. It introduces an entity above the user. With it come an admin who sees fifty people's completions and nobody else's, an invitation flow and a billing relationship. And a rule: one company's data never appears in another's.
The learners still share the catalog. So the answer isn't "make everything tenant-scoped". It's a per-table decision, which is why the measurement above is the right starting point.
Why learn this before it arrives
Because the choice is expensive to change and cheap to prepare for.
The patterns in lesson 2 differ in migration cost, isolation and operational overhead, and switching between them later means a data migration across every tenant. The preparation, on the other hand, is nearly free. Know which tables would be shared, keep ownership scoping explicit instead of implicit, and don't add a globally unique index that would have to become per-tenant.
Lesson 6 is that preparation list, and none of it requires deciding anything today.
Key takeaways
- Measured on 2026-10-05: 33 tables, 13 with
user_id, and 16 domain tables without one. user_idsays who owns a row; a tenant key says whose partition it is. Same shape, different consequence when forgotten.Identity::Currentdeclares one attribute, and a tenant would live there.- The catalog tables are shared on purpose, and the partner tables already carry a partner key; a tenant key is a per-table decision.
- The choice is expensive to change and nearly free to prepare for.
Keep going with lesson 2
Three patterns
Compares row-level, schema-level and database-level tenancy by isolation, migration cost, connections and how each one leaks.
A free account also gives you the quizzes, your progress and notes, and the discussion.