Choosing Your Data Model
Primitive offers two storage systems — Documents and Databases — each designed for different use cases. Many apps use both — documents for personal/collaborative data, databases for app-wide shared data. This guide helps you decide which to use where.
At a Glance
| Documents | Databases | |
|---|---|---|
| Where data lives | Local-first, on the device | Server-side |
| Connectivity | Offline access, instant local reads | Requires network connectivity |
| Collaboration | Real-time concurrent editing with conflict-free merging | Live updates the writing function publishes to a channel |
| Access model | Per-document permission levels (reader / read-write / owner) | Read and written from server functions — each function's access gate and code decide who sees what |
| Schema | Schemaless — no migrations; declared models drive typed codegen | Schemaless — no migrations; optional schema drives typed codegen |
| Size limit | ~10 MB per ordinary document; a large document (documentFormat: 2) is validated at 2 GB | ~5 GB per instance |
| Server logic | Server functions read and write document records (the typed document handle) | Server functions, plus server-enforced triggers and computed fields |
Use documents when users need offline access, real-time collaboration, or all users with access see the same data. Use databases when different users need different views of the data, you need server-enforced rules, or datasets are large.
When to Use Documents
Documents are local-first collaborative containers. Data lives on the device, syncs automatically, and works offline. Choose documents when:
- Users own the data — Tasks, notes, projects, personal records
- Real-time collaboration matters — Multiple users editing shared data simultaneously with conflict-free merging
- Offline access is important — Users need to work without a network connection
- Data is naturally partitioned by sharing context — Different people have access to different data sets
Documents are the right choice for the "inner" data of most productivity and collaboration apps. An ordinary document works best under ~10 MB. Past that, when every member of the sharing unit still needs all of the data, create the document as a large document (documentFormat: 2) instead of splitting it across several documents or moving it to a database: it is validated at 2 GB and keeps the ordinary document API. See Large Documents for which clients can open one and what differs.
When to Use Databases
Databases are server-side structured storage that your server functions read and write. Individual databases hold up to ~5 GB each. Choose databases when:
- Server-enforced rules are required — Validation, computed fields, or business logic that must run server-side
- Data is shared across all users — Product catalogs, configuration, lookup tables, shared settings
- You need cross-user queries — Leaderboards, directories, marketplace listings, aggregated reports
- Different users need different slices of the same data — Some can read but not write, or can only modify their own records; the function that serves them decides
- Admin manages the content — Content that's curated centrally, not user-generated
Partitioning Databases for Scale
A single global database works well when your data will stay under ~5 GB. But if there are natural ways to partition your data — by user, organization, team, or other context — and most queries stay within a single partition, consider creating separate databases from the start. This gives you scaling benefits for free and avoids a costly migration later. For example, an app where each organization has its own project data can use a database per organization rather than one large shared database.
Using Both Together
Most non-trivial apps benefit from both systems. Here are some patterns:
Classroom / LMS App
- Database for courses, assignments, and grade records (admin-managed, cross-student queries)
- Documents for student submissions and collaborative group work (student-owned, real-time collaboration)
E-Commerce / Marketplace
- Database for product catalog, orders, and inventory (server-enforced stock management, cross-user queries)
- Documents for shopping carts, wishlists, and saved preferences (user-owned, offline-capable)
Team Workspace
- Database for organization settings, role definitions, and shared configuration (admin-controlled)
- Documents for project data, notes, and collaborative content (team-owned, real-time sync)
Chat / Messaging
- Documents for message history within channels (real-time sync, per-channel sharing)
- Database for channel directory, user profiles, and cross-channel search indexes (cross-user queries)
Multi-Tenant SaaS
- Database for tenant configuration, billing state, and feature flags (server-enforced invariants)
- Documents for per-tenant collaborative workspaces (tenant-scoped, real-time collaboration)
Design Principles
Start with the sharing model. If different users need different access to different data, those boundaries often map to documents. Shared reference data goes in a database.
Don't fight the grain. If you need server-enforced validation or cross-user queries, use a database — don't try to build that on top of documents. If you need offline access and real-time collaboration, use documents — don't try to replicate that with a database.
Use groups for access control. Both documents and databases integrate with Primitive's group system. Groups let you model teams, roles, and relationships without building custom permission logic.
Keep documents focused. A document works best under ~10 MB. If data grows large and the sharing unit is naturally splittable, split across multiple documents by natural boundaries (per-project, per-channel, per-time-period). If it isn't — one project's full history, say — create it as a large document instead.
Common Mistakes
| Mistake | Why It's a Problem | Better Approach |
|---|---|---|
| Storing admin-managed content in documents | No server-side enforcement; any editor can change anything | Use a database, written only through a function gated to admins |
| Building cross-user queries on documents | Documents are isolated per user/sharing group — no global queries | Use a database for data that needs cross-user visibility |
| Using databases for real-time collaborative editing | Databases don't have built-in conflict resolution or offline sync | Use documents for collaborative data |
| One giant document for everything | Performance degrades past ~10 MB; can't share subsets independently | Split into multiple documents by sharing context, or create it as a large document (documentFormat: 2) when the whole sharing unit needs all of the data |
Record identity and external IDs
Let Primitive assign the primary record ID as a ULID. Keep an external system's identifier in a separate field, such as plaidTransactionId, stripeCustomerId, or externalId. Relationships between app records should reference their Primitive IDs.
A provider identifier or a key assembled from business fields is a lookup value, not the primary record ID. Declare the appropriate unique field or composite constraint for deduplication, including the provider or account scope when the external ID is only unique within that scope. On a retry, look up that identity and reuse the existing record; generating another ULID alone does not make an import idempotent.
For model creates, omit the ID and use the model's automatic assignment. When an API requires an explicit ID, such as a document bulk create, use Primitive's ULID generator. In a function started as a task (start), generate those IDs inside step.do so a replay reuses them.
Next Steps
- Working with Documents — Implement local-first collaborative data
- Working with Databases — Implement server-side structured storage
- Users and Groups — Set up access control for both systems