When this matters
These decisions are hard to reverse. A partner integration that uses VOMO User IDs as primary keys throughout will struggle when the customer migrates to a different platform; one that uses partner-assigned IDs survives the migration.
Principle 1: stable partner identifiers
Don’t make VOMO IDs your primary keys. Use partner-assigned identifiers as the stable reference, and link them to VOMO IDs via a mapping table.The pattern
partner_person_id is your stable key. The mapping table is the bridge to VOMO. The same pattern extends to Projects, Groups, Forms, etc.
Why not just use VOMO IDs
The partner-assigned ID isolates you from all of these.
Identifier generation
A few options forpartner_person_id:
For most integrations, UUID is the safe default. The opacity is a feature — it prevents IDs from being misused as ordering or identifying information.
Principle 2: mapping tables, not foreign keys
When linking partner-side records to VOMO records, use a mapping table rather than embeddingvomo_user_id as a foreign key everywhere.
The pattern
When mapping tables get complex
For multi-API integrations (Volunteer + CRM+ + Raise), the mapping pattern scales:Principle 3: snake_case → camelCase translation at the boundary
VOMO usessnake_case for all fields. Your application code probably uses something else (camelCase in JavaScript, PascalCase in C#, etc.). Translate at the API client boundary, not throughout your code.
The pattern
JavaScript
Defensive translation for audit-flagged quirks
The Volunteer API has documented quirks (see audits) that production code should handle defensively:JavaScript
Principle 4: model the participation challenge explicitly
Participations are the most operationally challenging part of any Volunteer integration. They have no direct endpoint, they live embedded in two different resources (User detail and Project Date detail), and they change post-event in ways that aren’t always polled correctly.The pattern: dedicated participations table keyed by (project_date_id, user_id)
vomo_disappeared_at column lets you keep history when VOMO removes a participation (organizer correction, Project deletion) — historical reports remain accurate.
Tracking changes to participation fields
Participation fields change over time —checked_in_at is set when the volunteer arrives; hours may be adjusted post-event. For audit-quality reporting, track changes:
Principle 5: denormalize for query, normalize for truth
VOMO’s API returns denormalized data (participations array embedded on UserDetailResource, participants embedded on ProjectDateResource). Don’t blindly mirror this — model normalized canonical data, then denormalize for query-specific views.
Canonical layer (normalized)
Query layer (denormalized)
When to denormalize
The general rule: normalize first, denormalize when a specific workload demands it.
Principle 6: store snapshots for change detection
For change-detection patterns (schedule diffs, member-list diffs), store the previous snapshot so you can compute deltas:Why JSONB for the snapshot
The snapshot isn’t queried — it’s only used for diff comparison. JSONB (or your DB’s JSON type) lets you store the entire schedule structure without designing a normalized schema for it. The diff code parses both old and new JSON into in-memory objects and compares. For PostgreSQL specifically, JSONB has good compression and is reasonably fast. For other databases, consider a serialized format that your application can deserialize cheaply.Principle 7: customer is a top-level dimension
Almost every table should havecustomer_id as part of its primary key or composite key:
Customer-scoped queries everywhere
JavaScript
customerId as the first argument. This makes accidental cross-customer access syntactically harder.
Principle 8: model what VOMO doesn’t expose
Sometimes the most valuable data in your model is data VOMO doesn’t have but the customer needs. Partner-side fields you might add:
These are partner-side facts; they don’t belong in VOMO. Modeling them explicitly lets the partner integration provide value beyond a pure mirror.
Anti-patterns to avoid
A few common modeling mistakes:Anti-pattern: storing VOMO’s raw response
Some integrations dump the entire VOMO response as JSON into a single column. This is appealingly simple but creates problems:- Queries on specific fields require JSON path expressions (slow and verbose)
- Schema evolution is invisible — when VOMO adds a field, you don’t know unless you re-process
- Translating snake_case to your conventions has to happen at every read site
Anti-pattern: assuming VOMO IDs are stable
VOMO IDs are stable in normal operation, but partner integrations should defensively handle the possibility that they aren’t:JavaScript
Anti-pattern: not modeling deletions
Many integrations don’t handle deletions explicitly — when a VOMO record disappears, the partner record just sits orphaned. Better:- Detect deletions via reconciliation
- Mark partner records with
vomo_disappeared_atrather than deleting them - Surface “this record no longer exists in VOMO” in the partner UI
- Decide per-record whether to cascade or preserve
Anti-pattern: shared tables across customers
A singleusers table containing all customers’ users sounds efficient but introduces:
- Risk of cross-customer access bugs
- Hard-to-isolate performance issues (one customer’s bulk operation slows everyone)
- Complex deletion when a customer offboards
- Compliance complications (data sovereignty, retention policies)
A reference schema for a typical integration
Putting the principles together:vomo_disappeared_at for soft deletes, audit logging — apply broadly.
Where to go next
API Performance Tips
The caching and performance patterns that make this data model fast.
Error Recovery Patterns
The resilience patterns that protect this data model from API issues.
Sync Architecture Patterns
The broader architectural patterns this modeling fits into.
The Volunteer Data Model
VOMO’s own data model — the source these patterns map from.