Files
Alejandro Gutiérrez 3527e732d4 feat: turbostarter boilerplate
Production-ready Next.js boilerplate with:
- Runtime env validation (fail-fast on missing vars)
- Feature-gated config (S3, Stripe, email, OAuth)
- Docker + Coolify deployment pipeline
- PostgreSQL + pgvector, MinIO S3, Better Auth
- TypeScript strict mode (no ignoreBuildErrors)
- i18n (en/es), AI modules, billing, monitoring

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 01:01:55 +01:00

4.9 KiB
Raw Permalink Blame History

title, description, url
title description url
Data model Entities and relationships for organizations and multi-tenancy. /docs/web/organizations/data-model

Data model

Our multi-tenant model is organized around the concept of an organization. An organization represents a single tenant and is the primary boundary for data isolation, access control, and routing.

Users can belong to multiple organizations through a membership. Invitations let organization admins onboard new members by email with a specific role.

Entities

Organization

The tenant. Stores human-friendly name, unique slug (used in URLs and lookups), optional logo, and optional metadata for extensibility (feature flags, billing context, UI preferences, etc.). createdAt provides auditability. The slug is globally unique to keep URLs stable and predictable.

User

The identity of a person. Users are global and can join many organizations. Account-level fields (e.g., name, email, verification, avatar, security flags) live here.

A user's application-wide properties (like a global `role` or moderation flags) are distinct from their per-organization role.

Member (Membership)

The join between a user and an organization. This is where multi-tenancy permissions are enforced. Each membership stores the role the user holds in that specific organization (for example, member, admin).

Memberships include timestamps for auditing and can be cascaded when a user or organization is removed.

Invitation

Represents an invite to join an organization by email with an intended role. It includes status (e.g., pending, accepted, revoked), expiresAt, and inviterId for traceability.

On acceptance, an invitation creates a corresponding membership if one does not already exist.

Relationships and constraints

Users and organizations are related many-to-many through memberships. A user can join multiple organizations; an organization has multiple members. We keep `organization.slug` unique across the system to ensure consistent routing and discoverability. Within a single organization, each `userId` should only appear once in memberships; enforce this at the application layer or with a composite unique index `(organizationId, userId)`. * Deleting an organization removes its dependent memberships and invitations. * Deleting a user removes their memberships and invitations.
These cascades preserve referential integrity and prevent orphaned records.

Tenancy and isolation

Tenant separator

organizationId is the tenant key. All tenant-scoped data should either live under the organization or reference it directly. Every read/write path in the application should be constrained by the current organizationId.

Query guardrails

Derive the active organizationId from authenticated context (session or URL slug → lookup → id). Apply organizationId filters at the repository/service layer to avoid crosstenant reads. Add composite indexes that include organizationId on frequently queried relations.

Isolation level

All organizations share the same database and schema, separated by organizationId. This keeps operations simple and costeffective. If stricter isolation is needed, evolve toward schemapertenant or databasepertenant with care, as operational overhead increases.

The term "organizations" is used throughout the starter kit to identify a group of users. However, depending on your application's needs, you might want to represent these groups with a different name, such as "Teams" or "Workspaces."

If that's the case, we suggest retaining "organization" as the internal term within your codebase (to avoid the complexity of renaming it everywhere), while customizing the UI labels to your preferred terminology. To do this, simply update all user-facing instances of "Organization" in your interface to reflect the term that best fits your application.

Lifecycle flows

  • Create organization: Create an organization (with name, slug, optional logo/metadata) and immediately create a membership for the creator with an elevated role (commonly owner).
  • Invite member:
    1. Admin creates an invitation specifying email and intended role.
    2. The invite is sent by email with an expiring token.
    3. On acceptance, if the user exists they are added as a member; otherwise they register and then join.
    4. Handle idempotency so repeated accepts dont duplicate memberships.
  • Leave or remove: Members can leave an organization and admins can remove members. The policy that "at least one owner must remain" is enforced at the application layer.