Overview
SnackBase enables Software-as-a-Service (SaaS) applications by allowing multiple independent tenants (accounts) to coexist in a single database while maintaining complete data isolation.Key Characteristics
| Characteristic | Description |
|---|---|
| Isolation Type | Row-level isolation via account_id column |
| Database Model | Shared database, shared tables |
| Account Scope | All data (users, collections, records) scoped to account_id |
| Cross-Account Access | Not possible by design (enforced at database and API levels) |
Account Model
What is an Account?
An Account (also called a “tenant” or “organization”) represents an isolated workspace containing:- Users who belong to the account
- Collections (data schemas) defined for the account
- Records (actual data) created by the account’s users
- Roles and permissions specific to the account
- Groups for organizing users
- Configuration overrides for providers (auth, email, storage)
Account Hierarchy
Data Isolation
How Isolation Works
Most tables in SnackBase include anaccount_id column that references the accounts table:
Tables WITHOUT account_id (Global Tables)
The following tables do not have anaccount_id column because they define global structures shared by all accounts:
| Table | Why No account_id? |
|---|---|
accounts | Defines accounts themselves (cannot be scoped to an account) |
roles | Roles are global definitions shared by all accounts |
permissions | Permissions are global rules shared by all accounts |
collections | Collection schemas are global definitions (data is isolated) |
macros | Macros are global SQL snippets shared by all accounts |
migrations | Migrations are global and affect all accounts |
Automatic Filtering
SnackBase automatically filters all queries byaccount_id. Users never see data from other accounts.
Example API Request:
account_id—it’s automatically added based on their authentication context.
Enforcement Layers
Isolation is enforced at multiple layers for defense-in-depth:| Layer | Mechanism | Details |
|---|---|---|
| Database | account_id column with foreign key to accounts | Row-level filtering at SQL level |
| Hook | account_isolation_hook (priority -200) | Automatically injects account_id filters |
| Repository | All repositories enforce account_id in queries | Cannot bypass without explicit override |
| API Middleware | Authorization middleware validates account context | Checks permissions before execution |
| Superadmin Bypass | Superadmin can pass account_id=None | Allows cross-account visibility for admins |
Two-Tier Architecture
SnackBase uses a two-tier table architecture that’s critical to understand:Tier 1: Core System Tables
These tables define the platform structure and are shared across all accounts:| Table | Purpose | Has account_id? | Schema Changes |
|---|---|---|---|
accounts | Account/tenant definitions | No (defines accounts) | Releases only |
users | User identities (per-account) | Yes | Releases only |
roles | Role definitions | No (global) | Releases only |
permissions | Permission rules | No (global) | Releases only |
collections | Collection schema definitions | No (global) | Releases only |
macros | SQL macro definitions | No (global) | Releases only |
migrations | Database migration history | No (global) | Automatic |
Tier 2: User-Created Collections
User collections are single physical tables shared by ALL accounts:| Physical Table | Collection Name | Contains |
|---|---|---|
col_posts | ”posts” | All accounts’ post data |
col_products | ”products” | All accounts’ product data |
col_orders | ”orders” | All accounts’ order data |
- A schema definition in the
collectionstable (metadata) - A physical table named
col_posts(if it doesn’t exist) - All accounts’ post data goes into this single shared table
Physical Table Naming Convention
Collection tables are prefixed withcol_ to avoid conflicts with system tables:
| Collection Name | Physical Table Name | Example Query |
|---|---|---|
posts | col_posts | SELECT * FROM col_posts WHERE account_id = ? |
products | col_products | SELECT * FROM col_products WHERE account_id = ? |
user_profiles | col_user_profiles | SELECT * FROM col_user_profiles WHERE account_id = ? |
- Prevents naming conflicts with system tables
- Makes it clear which tables are user-created collections
- Allows easy identification of collection tables in database dumps
Why This Architecture?
| Approach | Description | SnackBase Choice |
|---|---|---|
| Separate Tables | Each account gets their own col_posts_AB1001, col_posts_XY2048 tables | ❌ Not scalable (thousands of tables) |
| Separate Databases | Each account gets their own database | ❌ Complex operations and migrations |
| Shared Tables | All accounts share one col_posts table with account_id | ✅ Chosen for scalability and simplicity |
Account Identifiers
Accounts have three distinct identifiers that serve different purposes:Identifier Comparison
| Field | Format | Purpose | Example | Uniqueness |
|---|---|---|---|---|
id | UUID (36 chars) | Primary key, foreign key references | 550e8400-e29b-41d4-a716-446655440000 | Globally unique |
account_code | XX#### (6 chars) | Human-readable identifier | AB1234 | Globally unique |
slug | URL-friendly | Login and URL routing | acme-corp | Globally unique |
name | Free text | Display name only | Acme Corporation | Not unique |
Account ID (UUID)
The internal primary key for accounts is a standard UUID:- Purpose: Primary key, used in foreign key references
- Format: Standard UUID v4 (36 characters)
- Used by:
account_idcolumns in all tenant-scoped tables - Human-readable: No (designed for systems, not humans)
Account Code (XX####)
The human-readable identifier for accounts:- Letters (XX): Random uppercase letters A-Z
- Digits (####): Sequential number starting from 0001
- Total Capacity: 6,760,000 unique codes (26×26×10,000)
- Reserved Range: SY#### (skipped during generation)
Account Code Generation
Account codes are generated sequentially from the highest existing code:- Codes are never reused
- Sequential generation ensures predictability
- SY#### range is permanently reserved
- System account uses SY0000
Identifier Usage
| Identifier | Used In… | Example |
|---|---|---|
| id (UUID) | Foreign keys, account_id columns | WHERE account_id = '550e8400-...' |
| account_code | Admin UI, support, logs | ”Account AB1234” |
| slug | Login URLs, subdomain routing | ab1234.snackbase.com or /api/v1/accounts/acme-corp |
| name | UI display, emails | ”Welcome to Acme Corporation” |
System Account vs User Accounts
System Account (SY0000)
The system account is a special reserved account for superadmin operations:| Attribute | Value |
|---|---|
| ID | 00000000-0000-0000-0000-000000000000 (nil UUID) |
| Account Code | SY0000 (fixed) |
| Name | ”System” |
| Purpose | Superadmin operations, system-level configuration |
| Access | Superadmin users can operate across ALL accounts |
| Data | Contains minimal data (mostly metadata and system configs) |
- Access to ALL accounts
- Ability to create/manage accounts
- Ability to manage global collections
- System-wide visibility (can pass
account_id=Noneto see all data)
User Accounts
User accounts are regular tenant accounts created by superadmins:| Attribute | Value |
|---|---|
| ID | Auto-generated UUID (e.g., 550e8400-e29b-41d4-a716-446655440000) |
| Account Code | Auto-generated (e.g., AB1001) |
| Name | User-defined (e.g., “Acme Corporation”) |
| Purpose | Regular tenant operations |
| Access | Users can only access THEIR account |
| Data | Contains all tenant data (users, collections, records) |
- Access ONLY to their account
- No cross-account visibility
- Full CRUD within their account (based on permissions)
Multi-Account Users
Enterprise Multi-Account Model
SnackBase supports enterprise multi-account scenarios where a single user can belong to multiple accounts with different roles and permissions.User Identity
A user’s identity is defined by the (email, account_id) tuple:[email protected]) can exist in multiple accounts with different roles.
Password Scope
Passwords are per-account, not per-email. This means:[email protected]in accountAB1001has passwordPassword1![email protected]in accountXY2048has passwordPassword2!- These are different credentials even though the email is the same
Login Flow
When logging in, users must specify their account: Option 1: Account in URLConfiguration Hierarchy
SnackBase uses a hierarchical configuration model for provider settings (authentication, email, storage, etc.):Two-Level Hierarchy
Configuration Resolution
When resolving a provider configuration:- Check account-level config for the specific account
- If not found, use system-level default
- Merge with fallback values for any missing keys
Use Cases
| Configuration Type | System-Level | Account-Level |
|---|---|---|
| SMTP Settings | Default SMTP server | Custom SMTP per account |
| OAuth Providers | Available to all | Custom app credentials |
| Storage Backends | Default S3 bucket | Per-account buckets |
| Auth Providers | Default providers | Custom provider config |
Key Points
- System-level configs use the nil UUID (
00000000-0000-0000-0000-000000000000) - Account-level configs use the account’s UUID as
account_id - Resolution is cached for 5 minutes for performance
- Built-in providers are marked with
is_builtinflag (cannot be deleted)
Implications for Developers
When Building Applications
Understanding multi-tenancy is critical when building on SnackBase:1. Never Store Account ID Manually
2. Account Isolation is Automatic
You don’t need to write WHERE clauses for account filtering:3. Cross-Account Queries Are Impossible
By design, you cannot query across accounts:account_id=None to bypass filtering:
4. Collections Are Global
When creating a collection, remember:- The collection schema is shared across ALL accounts
- The physical table (
col_<name>) is shared across ALL accounts - Each account only sees their own data (via
account_idfiltering)
5. Migrations Affect All Accounts
Database migrations affect ALL accounts simultaneously:6. Use Account Code for Display
When displaying account identifiers in UI or logs:Summary
| Concept | Key Takeaway |
|---|---|
| Account Model | Accounts are isolated tenants with their own users, collections, and data |
| Account Identifiers | UUID (id) for system, account_code (XX####) for humans, slug for URLs, name for display |
| Data Isolation | Row-level isolation via account_id column, enforced at multiple layers |
| Global Tables | accounts, roles, permissions, collections, macros, migrations have no account_id |
| Two-Tier Architecture | Core system tables (release-only schema) + user collections (shared col_* tables) |
| System Account | Uses nil UUID for ID, SY0000 for account_code, reserved for superadmin operations |
| Multi-Account Users | Same email can exist in multiple accounts with different passwords |
| Configuration Hierarchy | System-level (nil UUID) defaults + account-level overrides |
| Developer Implications | Never handle account_id manually; isolation is automatic; collections are global |