Skip to main content
Every Volunteer integration eventually faces the same architectural question: how do you represent VOMO data inside your own system? It’s tempting to mirror VOMO’s structure exactly — but that couples your schema to API quirks, makes cross-API joins harder, and complicates evolution when VOMO changes. Better-designed integrations introduce a partner-side data model that’s inspired by VOMO’s shape but adapted to your use case. This page covers the modeling decisions that matter: identifier strategy, mapping tables, denormalization, the participation challenge, and the schema patterns that age well.

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

The 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 for partner_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 embedding vomo_user_id as a foreign key everywhere.

The pattern

The mapping table changes infrequently; the business tables don’t have to.

When mapping tables get complex

For multi-API integrations (Volunteer + CRM+ + Raise), the mapping pattern scales:
A single table maps partner IDs to any number of source-system IDs. Adding a new system (e.g., a future “Raise” integration) means adding rows, not new columns or tables.

Principle 3: snake_case → camelCase translation at the boundary

VOMO uses snake_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
Two big wins:

Defensive translation for audit-flagged quirks

The Volunteer API has documented quirks (see audits) that production code should handle defensively:
JavaScript
These helpers belong in your API client layer, not scattered through application code. When the API is eventually fixed (post-v2 overhaul), the fix is one file, not hundreds.

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)

The composite primary key is the natural unique identifier. The 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:
Adding entries on every change produces a complete event log of “Bruce’s hours were initially 4, changed to 3.75 a week later.” For compliance use cases this is essential.

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)

This is the truth. Updates happen here. Other tables derive from this.

Query layer (denormalized)

Materialized views refresh on a schedule (every hour or daily). Dashboards query the views; the canonical tables stay clean.

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:
On each polling cycle: fetch current, compare to most-recent snapshot, emit events for the diff, write the new snapshot. See Detecting Project Changes.

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 have customer_id as part of its primary key or composite key:
The composite key approach makes it impossible to accidentally write a query that crosses customer boundaries. Every WHERE clause must specify the customer.

Customer-scoped queries everywhere

JavaScript
Every data-access function takes 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
Better: extract the fields you care about into proper columns; keep the raw JSON only if you have a specific reason (e.g., debugging integration issues).

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
If VOMO IDs change (rare but possible during customer migrations), the mapping is the single source of truth — business records stay untouched.

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_at rather 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 single users 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)
Better: per-customer keys throughout (the Principle 7 pattern). For true multi-tenant isolation, consider per-customer schemas or databases.

A reference schema for a typical integration

Putting the principles together:
Modify for your specific needs, but the shapes — partner-assigned IDs, mapping tables, customer-scoped composite keys, 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.
Last modified on May 22, 2026