integrations — Phase 5
Schema locked 2026-06-11. 9 tables, 127 cols: connector (13), connector_credential (13), sync_run (18), sync_run_item (14), sync_error (14), provider_call (16), connector_webhook_event (12), webhook_delivery (16), field_mapping (11).
Integrations is the generic external-connector RUNTIME. One connector framework reused across all connector types — NOT per-integration tables. It executes API calls, runs sync jobs, processes inbound webhooks for its own connectors, delivers outbound webhooks, and logs/retries. All tables are tenant-scoped with RLS except connector_webhook_event (nullable tenant_id; written by webhook handler via service_role).
Design principles (document at every session touching this schema):
- Config vs. runtime.
admin.integration_configowns the config (enabled/disabled toggle,credentials_refvault pointer,sync_schedule).admin.webhook_configowns outbound webhook endpoint definitions (URL, event subscriptions,secret_ref). Integrations owns the runtime: it reads Admin config at execution time and writes backlast_sync_at/last_sync_statustoadmin.integration_configafter each sync run. This is a controlled cross-schema write — the same pattern as Billing writing backpaid_at/payment_status_reftopurchasing.vendor_invoice. Never duplicate config columns into the Integrations schema. - Reference-don't-copy.
provider_calllogs the mechanical API-call state: did the Resend/Twilio/FCM call succeed, get rate-limited, enter retry? It stores theprovider_message_refthat Notifications stores ondelivery_attempt. It does NOT store delivery outcome, engagement, recipient, or content — those belong tonotifications.delivery_attempt(the authoritative business record).provider_callexists for connector-layer observability (rate-limit debugging, retry state, dead-letter), not as a delivery duplicate. - Owner-processes-own-webhooks. Integrations processes inbound webhooks for its own connectors (QuickBooks change notifications, Twilio inbound SMS, etc.) via
connector_webhook_event. Payments owns Stripe's inbound webhooks (payments.stripe_event_log). There is no central webhook table spanning all modules. Rule: the module that owns the integration owns that integration's inbound webhooks. - Source type. A connector's source is
'api'(API-based sync — QuickBooks, Shopify),'file'(deterministic known-format file import — Picas CSV, templated bulk load, viafield_mapping+sync_run), or'webhook'(event-driven — inbound webhook triggers processing). File source here means deterministic/known-format imports only. AI zero-mapping import (upload a random CSV → AI identifies structure → loads data) is NOT here — that belongs to the AI module. - Auth providers are not here. SSO/SAML/OIDC/social-login authentication providers are handled by Supabase Auth +
identity.sso_providerconfig. Integrations connects to business systems (QuickBooks, Shopify, notification providers) — never to auth providers. Do not add auth-provider connectors to this schema. - Rate-limit state is not a table. Live rate-limit windows are ephemeral runtime state (Redis in-memory windows).
provider_calllogs individual calls for observability; the live sliding-window counter is never persisted to Postgres. This is a conscious non-table decision.
Cross-Phase FK seams:
| Column | References | Status |
|---|---|---|
*.tenant_id |
platform.tenant |
Locked — enforced |
connector.integration_config_id |
admin.integration_config |
Locked — enforced (1:1 runtime anchor; reads config; writes back sync status) |
connector_credential.connector_id, sync_run.connector_id, sync_run_item.connector_id, sync_error.connector_id, provider_call.connector_id, connector_webhook_event.connector_id, field_mapping.connector_id |
integrations.connector |
Intra-schema — enforced |
sync_error.sync_run_id, sync_run_item.sync_run_id |
integrations.sync_run |
Intra-schema — enforced (nullable) |
connector.last_sync_run_id |
integrations.sync_run |
Intra-schema — enforced (nullable; denormalized pointer) |
webhook_delivery.webhook_config_id |
admin.webhook_config |
Locked — enforced (Integrations reads outbound endpoint def) |
connector_webhook_event.tenant_id |
platform.tenant |
Nullable — resolved post-receipt; same pattern as payments.stripe_event_log |
provider_call.provider_message_ref |
notifications.delivery_attempt.provider_message_ref |
Read seam — not an FK; Notifications stores this ref after IntegrationsService.send() returns it |
connector_webhook_event.routed_to |
Downstream table (e.g. notifications.inbound_message for Twilio inbound SMS) |
Text seam — records where the event was routed; not an FK |
sync_run_item.local_ref |
Any Vrida record (polymorphic — inventory.item_variant, crm.customer, purchasing.vendor_invoice, etc.) |
Polymorphic — NOT enforced FK; entity_type is the discriminator |
sync_run.file_ref |
Files module / R2 key | Text seam — Files module not built |
Integrations WRITES BACK last_sync_at/last_sync_status to admin.integration_config |
admin schema |
Write seam — controlled cross-schema write via AdminService; not a FK |
Deferred items:
- v1.5 connectors: Shopify / WooCommerce / Wix (eCommerce sync), Sage / Xero / Sage Intacct (accounting), Mailchimp / SendGrid (marketing) — framework supports via
connector_type; built later. - Workforce sync (Gusto / ADP / Deputy) — likely not built: HR is permanently out of scope for Vrida (cut 2026-06-10). These connectors lose their purpose without the HR module.
- AI zero-mapping import pipeline (
import_job/import_file/import_record) — relocated to AI module per PROJECT_DECISIONS "Integrations Module Scope (2026-06-11)". Not here. - Rate-limit window state — ephemeral runtime (Redis); not a table by design.
- L3 (deferred):
sync_runcomposite indexON (connector_id, started_at)— add when sync-run dashboard time-range queries are built.
integrations.connector — 13 cols
Runtime anchor — one row per active integration instance. The live operational counterpart to admin.integration_config (the config row). Tracks connection health, current status, and a denormalized pointer to the most recent sync run. connector_type is a free-text discriminator (e.g. 'quickbooks', 'resend', 'twilio') — the generic framework uses this to route connector-specific logic.
RLS: tenant-isolated on tenant_id.
| Column | Type | Nullable | Default | Constraints / Notes |
|---|---|---|---|---|
id |
UUID | NOT NULL | uuid_generate_v4() |
PK |
tenant_id |
UUID | NOT NULL | — | FK → platform.tenant |
created_at |
timestamptz | NOT NULL | now() |
|
updated_at |
timestamptz | NOT NULL | now() |
|
deleted_at |
timestamptz | nullable | — | Soft delete |
integration_config_id |
UUID | NOT NULL | — | FK → admin.integration_config — the config row this runtime serves; 1:1 |
connector_type |
text | NOT NULL | — | Free-text discriminator (e.g. 'quickbooks', 'resend', 'twilio', 'fcm', 'picas_csv', 'shopify'). Generic — no enum; new connectors are a new value, not a schema change. |
source_type |
text | NOT NULL | — | CHECK IN ('api', 'file', 'webhook') — 'api' = API-based sync; 'file' = deterministic known-format import; 'webhook' = event-driven |
status |
text | NOT NULL | 'disconnected' |
CHECK IN ('connected','disconnected','error','connecting') — RUNTIME operational status, distinct from admin.integration_config.is_enabled (the config toggle) |
last_handshake_at |
timestamptz | nullable | — | Last successful connection check / OAuth ping |
last_sync_run_id |
UUID | nullable | — | FK → integrations.sync_run — most recent run; denormalized pointer for dashboard queries |
health |
text | nullable | — | CHECK (health IS NULL OR health IN ('healthy','degraded','failing')) |
error_message |
text | nullable | — | Current error detail when status = 'error' |
Indexes:
- PK on
id - on (
tenant_id) - on (
connector_type) - on (
status) WHEREstatus IN ('error','connecting') - UNIQUE on (
tenant_id,integration_config_id) WHEREdeleted_at IS NULL— one runtime connector per config row
integrations.connector_credential — 13 cols
OAuth runtime token state — rotates frequently (typically hourly). Distinct from admin.integration_config.credentials_ref, which is the static config pointer to the initial OAuth grant. This table holds the live access/refresh token vault references and their expiry state, enabling the connector to refresh tokens without touching Admin's config row.
All token refs are vault references — never raw tokens. The expires_at column drives the proactive refresh sweep.
RLS: tenant-isolated on tenant_id.
| Column | Type | Nullable | Default | Constraints / Notes |
|---|---|---|---|---|
id |
UUID | NOT NULL | uuid_generate_v4() |
PK |
tenant_id |
UUID | NOT NULL | — | FK → platform.tenant |
created_at |
timestamptz | NOT NULL | now() |
|
updated_at |
timestamptz | NOT NULL | now() |
|
deleted_at |
timestamptz | nullable | — | Soft delete |
connector_id |
UUID | NOT NULL | — | FK → integrations.connector |
credential_type |
text | NOT NULL | — | CHECK IN ('oauth2','api_key','basic','token') |
access_token_ref |
text | nullable | — | Vault reference — the live access token; never stored raw |
refresh_token_ref |
text | nullable | — | Vault reference — the refresh token; never stored raw |
expires_at |
timestamptz | nullable | — | Access token expiry — the refresh sweep targets this column |
scope |
text | nullable | — | OAuth granted scopes (e.g. 'com.intuit.quickbooks.accounting') |
last_refreshed_at |
timestamptz | nullable | — | When the access token was last refreshed |
status |
text | NOT NULL | 'active' |
CHECK IN ('active','expired','revoked','refresh_failed') |
Indexes:
- PK on
id - on (
tenant_id) - on (
connector_id) - on (
expires_at) WHEREstatus = 'active'— refresh sweep: find credentials expiring soon - UNIQUE on (
connector_id) WHEREdeleted_at IS NULL— one active credential set per connector
integrations.sync_run — 18 cols
One row per sync job execution. Mutable: status progresses from 'queued' → 'running' → terminal state; record counts update as processing proceeds. The run_type discriminator covers the full range from scheduled batch syncs to file imports to webhook-triggered incremental syncs. Integrations writes last_sync_at / last_sync_status back to admin.integration_config when a run completes.
NOT append-only — status, records_succeeded, records_failed, completed_at, failure_reason all mutate as the run progresses.
RLS: tenant-isolated on tenant_id.
| Column | Type | Nullable | Default | Constraints / Notes |
|---|---|---|---|---|
id |
UUID | NOT NULL | uuid_generate_v4() |
PK |
tenant_id |
UUID | NOT NULL | — | FK → platform.tenant |
created_at |
timestamptz | NOT NULL | now() |
|
updated_at |
timestamptz | NOT NULL | now() |
|
deleted_at |
timestamptz | nullable | — | Soft delete |
connector_id |
UUID | NOT NULL | — | FK → integrations.connector |
run_type |
text | NOT NULL | — | CHECK IN ('scheduled','manual','initial_import','webhook_triggered','file_import') |
direction |
text | NOT NULL | — | CHECK IN ('inbound','outbound','bidirectional') |
status |
text | NOT NULL | 'queued' |
CHECK IN ('queued','running','completed','failed','partial','cancelled') |
started_at |
timestamptz | nullable | — | When processing actually began |
completed_at |
timestamptz | nullable | — | When the run reached a terminal status |
records_total |
integer | nullable | — | Total records to process (NULL until known) |
records_succeeded |
integer | NOT NULL | 0 |
Running count of successfully processed records |
records_failed |
integer | NOT NULL | 0 |
Running count of failed records |
file_ref |
text | nullable | — | For run_type = 'file_import' — the uploaded file's R2 key. Text seam — Files module not built. |
trigger_ref |
text | nullable | — | What triggered the run (e.g. a connector_webhook_event.id for webhook-triggered runs) |
summary |
text | nullable | — | Human-readable run summary (e.g. 'Synced 142 invoices, skipped 3') |
failure_reason |
text | nullable | — | Top-level failure description for failed / partial runs |
Table-level CHECKs:
CHECK (run_type != 'file_import' OR file_ref IS NOT NULL)
A file-import run must carry its file reference. (file_ref is the R2 key of the uploaded file; required for file-import runs, null for all others.)
Indexes:
- PK on
id - on (
tenant_id) - on (
connector_id) - on (
status) WHEREstatus IN ('queued','running','failed') - on (
started_at) - on (
run_type)
integrations.sync_run_item — 14 cols
Per-record sync result — granular trail of what happened to each record in a sync_run. Provides the row-level audit needed to diagnose partial failures, replay failed items, and report on sync coverage. local_ref and external_record_id together form the cross-system identity map for each record.
RLS: tenant-isolated on tenant_id.
| Column | Type | Nullable | Default | Constraints / Notes |
|---|---|---|---|---|
id |
UUID | NOT NULL | uuid_generate_v4() |
PK |
tenant_id |
UUID | NOT NULL | — | FK → platform.tenant |
created_at |
timestamptz | NOT NULL | now() |
|
updated_at |
timestamptz | NOT NULL | now() |
|
deleted_at |
timestamptz | nullable | — | Soft delete |
sync_run_id |
UUID | NOT NULL | — | FK → integrations.sync_run |
connector_id |
UUID | NOT NULL | — | FK → integrations.connector — denormalized for direct connector-level queries without joining through sync_run |
direction |
text | NOT NULL | — | CHECK IN ('inbound','outbound') |
entity_type |
text | NOT NULL | — | What kind of record (e.g. 'invoice', 'customer', 'product', 'chart_of_accounts'). Connector-specific vocabulary. |
local_ref |
UUID | nullable | — | The Vrida record's id (the mapped local entity). Polymorphic — NOT enforced FK; entity_type is the discriminator. |
external_record_id |
text | nullable | — | The external system's record identifier (QuickBooks DocNumber, Shopify product id, etc.) |
operation |
text | NOT NULL | — | CHECK IN ('create','update','delete','skip') |
status |
text | NOT NULL | 'succeeded' |
CHECK IN ('succeeded','failed','skipped') |
error_detail |
text | nullable | — | Error message for failed items |
Indexes:
- PK on
id - on (
tenant_id) - on (
sync_run_id) - on (
connector_id) — connector-level item queries without joining throughsync_run - on (
status) WHEREstatus = 'failed' - on (
entity_type,local_ref) WHERElocal_ref IS NOT NULL— "which sync runs touched this Vrida record" - on (
external_record_id) WHEREexternal_record_id IS NOT NULL— "which Vrida record maps to this external id"
integrations.sync_error — 14 cols
Error and dead-letter log per connector. Collects errors that may span multiple sync runs (auth failures, persistent mapping failures) or are not tied to a specific run (connection errors). Retryable: retry_count and last_retry_at drive the retry loop; resolved_at marks closure.
RLS: tenant-isolated on tenant_id.
| Column | Type | Nullable | Default | Constraints / Notes |
|---|---|---|---|---|
id |
UUID | NOT NULL | uuid_generate_v4() |
PK |
tenant_id |
UUID | NOT NULL | — | FK → platform.tenant |
created_at |
timestamptz | NOT NULL | now() |
|
updated_at |
timestamptz | NOT NULL | now() |
|
deleted_at |
timestamptz | nullable | — | Soft delete |
connector_id |
UUID | NOT NULL | — | FK → integrations.connector |
sync_run_id |
UUID | nullable | — | FK → integrations.sync_run — the run this error occurred in, if any. NULL for connection-level errors not tied to a run. |
error_type |
text | NOT NULL | — | CHECK IN ('auth','rate_limit','validation','network','mapping','provider','other') |
error_message |
text | NOT NULL | — | Error description |
payload |
JSONB | nullable | — | The failing record or request payload, preserved for replay. Example: {"entity":"invoice","external_id":"INV-1001","error":"Validation error: tax_code missing","raw":{"DocNumber":"1001","TxnDate":"2026-06-11"}} |
retry_count |
integer | NOT NULL | 0 |
Number of retry attempts made |
last_retry_at |
timestamptz | nullable | — | When the last retry was attempted |
resolved_at |
timestamptz | nullable | — | When the error was resolved (retried successfully, manually resolved, or abandoned) |
resolution |
text | nullable | — | CHECK (resolution IS NULL OR resolution IN ('retried','manual','abandoned')) |
Indexes:
- PK on
id - on (
tenant_id) - on (
connector_id) - on (
error_type) - on (
resolved_at) WHEREresolved_at IS NULL— open dead-letter queue sweep - on (
last_retry_at) WHEREresolved_at IS NULL— retry-worker sweep: unresolved errors due for next retry attempt
integrations.provider_call — 16 cols
Thin mechanical API-call log for provider-executed sends (Resend email, Twilio SMS, FCM/APNS push). Records whether the API call to the provider succeeded, was rate-limited, or entered retry — the connector layer's operational view. This is NOT a delivery record. The provider_message_ref returned by the provider is what NotificationsService stores on notifications.delivery_attempt (the authoritative business record with delivery outcome + engagement).
Reference-don't-copy: provider_call owns mechanical call state; notifications.delivery_attempt owns business delivery + engagement. These serve different concerns: provider_call is for connector-layer debugging (rate limits, retry exhaustion, dead-letter); delivery_attempt is for business delivery reporting (delivered, opened, clicked).
NOT append-only — status, http_status, attempt_count, error_detail, completed_at all mutate as the call progresses and retries.
RLS: tenant-isolated on tenant_id.
| Column | Type | Nullable | Default | Constraints / Notes |
|---|---|---|---|---|
id |
UUID | NOT NULL | uuid_generate_v4() |
PK |
tenant_id |
UUID | NOT NULL | — | FK → platform.tenant |
created_at |
timestamptz | NOT NULL | now() |
|
updated_at |
timestamptz | NOT NULL | now() |
|
deleted_at |
timestamptz | nullable | — | Soft delete |
connector_id |
UUID | NOT NULL | — | FK → integrations.connector |
provider |
text | NOT NULL | — | The provider being called (e.g. 'resend', 'twilio', 'fcm', 'apns', 'quickbooks') |
call_type |
text | NOT NULL | — | The operation being executed (e.g. 'send_email', 'send_sms', 'send_push', 'sync_invoice') |
idempotency_key |
text | nullable | — | Outgoing call dedup key — prevents double-calling the provider on retry |
provider_message_ref |
text | nullable | — | The provider's ID returned on success (Resend email ID, Twilio SID, FCM message ID, etc.). This is what NotificationsService stores on notifications.delivery_attempt.provider_message_ref. |
status |
text | NOT NULL | 'queued' |
CHECK IN ('queued','sent','succeeded','failed','rate_limited','retrying'). STATUS STATE MACHINE: queued → sent (HTTP request dispatched + provider accepted, e.g. Resend/Twilio 202 Accepted — async, not yet confirmed) → succeeded (confirmed: synchronous 200 OK, or a provider-confirmed delivery response). For fire-and-forget providers (Resend, Twilio), 'sent' is often the terminal state for Integrations — delivery CONFIRMATION flows to notifications.delivery_attempt via inbound webhook, NOT back here. Error path: failed / rate_limited / retrying. Retry logic: retry 'failed'/'rate_limited' only — NOT 'sent' (already accepted by provider). |
http_status |
integer | nullable | — | HTTP response code from the provider |
attempt_count |
integer | NOT NULL | 0 |
Total attempts made (including retries) |
error_detail |
text | nullable | — | Provider error message or code |
requested_at |
timestamptz | NOT NULL | — | When the call was initiated |
completed_at |
timestamptz | nullable | — | When the call reached a terminal status |
Table-level CHECKs:
CHECK (status != 'succeeded' OR provider_message_ref IS NOT NULL)
A succeeded call must carry the provider message reference — this is the value NotificationsService cross-looks-up when storing the ref on notifications.delivery_attempt.
Indexes:
- PK on
id - on (
tenant_id) - on (
connector_id) - on (
status) WHEREstatus IN ('queued','failed','rate_limited','retrying') - on (
provider_message_ref) WHEREprovider_message_ref IS NOT NULL - UNIQUE on (
tenant_id,idempotency_key) WHEREidempotency_key IS NOT NULL AND deleted_at IS NULL
integrations.connector_webhook_event — 12 cols
Inbound webhook idempotency log for Integrations-owned connectors (QuickBooks change notifications, Twilio inbound SMS, etc.). Insert-then-update-status: status and processed_at mutate post-insert; no updated_at; no deleted_at — events are never deleted (same pattern as payments.stripe_event_log).
tenant_id is nullable. Some webhooks arrive before tenant resolution (the provider posts to Vrida's endpoint, not a tenant-specific URL). tenant_id is resolved from the connector context and set in the processing step. Same pattern as payments.stripe_event_log.tenant_id.
GLOBAL UNIQUE on external_event_id. Provider event IDs are globally unique (QuickBooks, Twilio, etc. generate UUIDs without tenant scope). The dedup must be global — if a webhook is retried before tenant resolution completes, a tenant-scoped unique would allow a second insert. Guard: do not change this to a partial or tenant-scoped unique.
routed_to: Twilio inbound SMS events are routed to notifications.inbound_message (Integrations receives, Notifications processes). This text field records the routing target for traceability.
RLS: Non-standard. Written by webhook handler via service_role (same as payments.stripe_event_log). tenant_id is nullable. Tenant-scoped reads work for resolved events; unresolved events are service-role only.
| Column | Type | Nullable | Default | Constraints / Notes |
|---|---|---|---|---|
id |
UUID | NOT NULL | uuid_generate_v4() |
PK |
tenant_id |
UUID | nullable | — | FK → platform.tenant — nullable; resolved post-receipt |
created_at |
timestamptz | NOT NULL | now() |
|
connector_id |
UUID | nullable | — | FK → integrations.connector — resolved from webhook source; may be null briefly during processing |
provider |
text | NOT NULL | — | Provider that sent the webhook (e.g. 'quickbooks', 'twilio') |
event_type |
text | NOT NULL | — | External event type (e.g. 'invoice.updated', 'inbound_sms', 'customer.updated') |
external_event_id |
text | NOT NULL | — | Provider's event ID — the global dedup key (NOT tenant-scoped; see Guard above) |
status |
text | NOT NULL | 'received' |
CHECK IN ('received','processed','ignored','failed') — mutates post-insert; 'received' is the only insert-time value |
payload |
JSONB | nullable | — | Raw webhook payload as received from the provider. Example (QuickBooks change notification): {"eventNotifications":[{"realmId":"1234567","dataChangeEvent":{"entities":[{"name":"Invoice","id":"9001","operation":"Update","lastUpdated":"2026-06-11T10:00:00Z"}]}}]} |
routed_to |
text | nullable | — | Where this event was routed for processing (e.g. 'notifications.inbound_message' for Twilio inbound SMS) |
processed_at |
timestamptz | nullable | — | When status moved to 'processed' or 'ignored' |
received_at |
timestamptz | NOT NULL | — | When the webhook was received from the provider |
Indexes:
- PK on
id - on (
tenant_id) - on (
connector_id) - on (
event_type) - UNIQUE on (
external_event_id) — GLOBAL, not partial, not tenant-scoped. Guard: do not change to a partial unique. Provider event IDs are globally unique; webhooks may arrive before tenant resolution. - on (
status) WHEREstatus IN ('received','failed')
integrations.webhook_delivery — 16 cols
Outbound webhook delivery execution — fires Vrida events at tenant-configured external endpoints. Reads admin.webhook_config for the endpoint definition (URL, subscriptions, secret_ref). target_url is snapshotted at send-time so the delivery record remains accurate if the endpoint is reconfigured. next_retry_at drives the retry backoff sweep.
NOT append-only — status, http_status, attempt_count, last_attempt_at, next_retry_at, response_body all mutate as delivery is attempted and retried.
RLS: tenant-isolated on tenant_id.
| Column | Type | Nullable | Default | Constraints / Notes |
|---|---|---|---|---|
id |
UUID | NOT NULL | uuid_generate_v4() |
PK |
tenant_id |
UUID | NOT NULL | — | FK → platform.tenant |
created_at |
timestamptz | NOT NULL | now() |
|
updated_at |
timestamptz | NOT NULL | now() |
|
deleted_at |
timestamptz | nullable | — | Soft delete |
webhook_config_id |
UUID | NOT NULL | — | FK → admin.webhook_config — the configured endpoint being delivered to |
event_type |
text | NOT NULL | — | The Vrida event being delivered (e.g. 'pos.sale_completed', 'inventory.stock_low', 'orders.order_fulfilled') |
event_ref |
UUID | nullable | — | The source record's id (the triggering entity). Not enforced FK — polymorphic (event_type is the discriminator). |
target_url |
text | NOT NULL | — | Snapshot of the endpoint URL at send-time — preserved for accuracy if the config is later changed |
status |
text | NOT NULL | 'pending' |
CHECK IN ('pending','delivering','delivered','failed','abandoned') |
http_status |
integer | nullable | — | HTTP response code from the external endpoint |
attempt_count |
integer | NOT NULL | 0 |
Total delivery attempts (including retries) |
last_attempt_at |
timestamptz | nullable | — | When the most recent delivery attempt was made |
next_retry_at |
timestamptz | nullable | — | When the next retry should be attempted (exponential backoff). The retry sweep indexes this column. |
response_body |
text | nullable | — | Truncated response body from the endpoint (for debugging) |
payload |
JSONB | nullable | — | The Vrida event envelope that was sent to the endpoint. Example: {"event":"pos.sale_completed","tenant_id":"<uuid>","occurred_at":"2026-06-11T10:00:00Z","data":{"sale_id":"<uuid>","total_cents":4299,"location_id":"<uuid>"}} |
Indexes:
- PK on
id - on (
tenant_id) - on (
webhook_config_id) - on (
status) WHEREstatus IN ('pending','delivering','failed') - on (
next_retry_at) WHEREnext_retry_at IS NOT NULL— retry sweep - on (
event_type)
integrations.field_mapping — 11 cols
Transformation config for data-sync connectors — maps external system fields to Vrida fields, optionally with transform rules. Used by both API-sync connectors (QuickBooks chart-of-accounts field map) and file-import connectors (Picas CSV column map). The Picas nursery-specific mapping is data stored here, not a dedicated schema table — generic-first principle in action.
Distinct from AI zero-mapping: field_mapping is the deterministic, human-configured or system-configured mapping for known-format data. AI zero-mapping (where the AI infers the structure from an arbitrary file) belongs to the AI module.
RLS: tenant-isolated on tenant_id.
| Column | Type | Nullable | Default | Constraints / Notes |
|---|---|---|---|---|
id |
UUID | NOT NULL | uuid_generate_v4() |
PK |
tenant_id |
UUID | NOT NULL | — | FK → platform.tenant |
created_at |
timestamptz | NOT NULL | now() |
|
updated_at |
timestamptz | NOT NULL | now() |
|
deleted_at |
timestamptz | nullable | — | Soft delete |
connector_id |
UUID | NOT NULL | — | FK → integrations.connector |
entity_type |
text | NOT NULL | — | The entity being mapped (e.g. 'invoice', 'product', 'customer', 'chart_of_accounts') |
direction |
text | NOT NULL | — | CHECK IN ('inbound','outbound','bidirectional') |
mapping |
JSONB | NOT NULL | — | The field map definition. Example: {"external_field":"DocNumber","local_field":"invoice_number","transform":"trim"} or an array of such entries. |
is_active |
boolean | NOT NULL | true |
Only active mappings are used during sync |
version |
integer | NOT NULL | 1 |
Mapping version — incremented when the mapping is revised |
Indexes:
- PK on
id - on (
tenant_id) - on (
connector_id) - on (
entity_type) - UNIQUE on (
tenant_id,connector_id,entity_type,direction,version) WHEREdeleted_at IS NULL
Column counts: connector(13) + connector_credential(13) + sync_run(18) + sync_run_item(14) + sync_error(14) + provider_call(16) + connector_webhook_event(12) + webhook_delivery(16) + field_mapping(11) = 127