Reactive data layer for schema-driven Cards, Procedures, and Deliverables.
Bridge is a Convex component that provides a structured approach to defining data fields, collection procedures, and reactive triggers. It's designed for applications that need to dynamically define what data to collect and when to trigger automated workflows.
- Cards - Field definitions with types, security levels, and subject associations
- Procedures - Data collection definitions (forms, imports, APIs) that specify which cards to collect
- Deliverables - Reactive triggers with prerequisites and conditions that fire when data is ready
- Evaluations - Execution tracking with scheduling, status management, and results
bun add @trestleinc/bridge// convex/convex.config.ts
import { defineApp } from 'convex/server';
import bridge from '@trestleinc/bridge/convex.config';
const app = defineApp();
app.use(bridge);
export default app;// convex/bridge.ts
import { bridge } from '@trestleinc/bridge/server';
import { components } from './_generated/api';
// Minimal configuration - hooks are optional
export const b = bridge.create(components.bridge, {
// Bind subject types to your host tables for automatic context resolution
subjects: {
beneficiary: { table: 'beneficiaries' },
event: { table: 'events' },
eventInstance: {
table: 'eventInstances',
parents: [{ field: 'eventId', subject: 'event' }],
},
},
});// convex/cards.ts
import { b } from './bridge';
// Export bridge resources directly as Convex functions
export const get = b.cards.get;
export const find = b.cards.find;
export const list = b.cards.list;
export const create = b.cards.create;Field definitions with types and security metadata.
| Method | Description |
|---|---|
b.cards.get |
Get a card by ID |
b.cards.find |
Find a card by organization and slug |
b.cards.list |
List cards for an organization |
b.cards.create |
Create a new card |
Data collection definitions (forms, imports, APIs).
| Method | Description |
|---|---|
b.procedures.get |
Get a procedure by ID |
b.procedures.list |
List procedures for an organization |
b.procedures.create |
Create a new procedure |
b.procedures.update |
Update an existing procedure |
b.procedures.remove |
Delete a procedure |
b.procedures.submit |
Validate card values against procedure schema |
Reactive triggers with conditions.
| Method | Description |
|---|---|
b.deliverables.get |
Get a deliverable by ID |
b.deliverables.list |
List deliverables for an organization |
b.deliverables.create |
Create a new deliverable |
b.deliverables.update |
Update an existing deliverable |
b.deliverables.evaluate |
Check and trigger ready deliverables |
Execution records for triggered deliverables.
| Method | Description |
|---|---|
b.evaluations.get |
Get an evaluation by ID |
b.evaluations.list |
List evaluations for a deliverable |
b.evaluations.start |
Start a scheduled evaluation |
b.evaluations.cancel |
Cancel a pending evaluation |
b.evaluations.complete |
Mark an evaluation as complete |
interface Card {
id: string;
organizationId: string;
slug: string;
label: string;
cardType: CardType; // 'STRING' | 'NUMBER' | 'BOOLEAN' | 'DATE' | 'EMAIL' | 'URL' | 'PHONE' | 'SSN' | 'ADDRESS' | 'SUBJECT' | 'ARRAY'
securityLevel: SecurityLevel; // 'PUBLIC' | 'CONFIDENTIAL' | 'RESTRICTED'
subject: string;
createdBy: string;
createdAt: number;
}interface Procedure {
id: string;
organizationId: string;
name: string;
description?: string;
procedureType: ProcedureType; // 'form' | 'import' | 'api'
subject?: {
type: string;
operation: Operation; // 'create' | 'update' | 'delete'
};
cards: ProcedureCard[]; // Cards to collect with field mappings
createdAt: number;
updatedAt: number;
}interface Deliverable {
id: string;
organizationId: string;
name: string;
description?: string;
subject: string;
operations: DeliverableOperations;
schedule?: Schedule;
status: DeliverableStatus; // 'active' | 'paused'
createdAt: number;
updatedAt: number;
}interface Evaluation {
id: string;
deliverableId: string;
organizationId: string;
operation: Operation;
context: EvaluationContext;
variables: Record<string, unknown>;
status: EvaluationStatus; // 'pending' | 'running' | 'completed' | 'failed'
scheduledFor?: number;
result?: EvaluationResult;
createdAt: number;
completedAt?: number;
}The bridge instance provides convenience methods for common operations:
Validate and submit card values through a procedure:
const result = await b.submit(ctx, {
procedureId: 'proc_123',
organizationId: 'org_456',
subject: 'beneficiary',
subjectId: 'ben_789',
values: { firstName: 'John', lastName: 'Doe' },
});
if (result.success) {
// Write validated values to your tables
}Trigger deliverable evaluation for a subject. If subjects are bound, variables are auto-resolved from the host table:
// With auto-resolution (subjects bound) - no variables needed!
const readiness = await b.evaluate(ctx, {
organizationId: 'org_456',
subject: 'beneficiary',
subjectId: 'ben_789',
operation: 'create',
});
for (const r of readiness) {
if (r.ready) {
console.log(`Deliverable ${r.deliverableId} triggered, evaluation ${r.evaluationId}`);
}
}Manually resolve subject data from a bound host table:
const variables = await b.resolve(ctx, 'beneficiary', 'ben_789');
// { firstName: 'John', lastName: 'Doe', email: 'john@example.com' }Register callback handlers and execute deliverables:
// Register handlers at module level
b.register('automation', async (deliverable, context) => {
// Execute automation logic
return { success: true, data: { sent: true } };
});
// Execute in an action
const result = await b.execute(deliverable, 'create', {
subject: 'beneficiary',
subjectId: 'ben_789',
variables: { firstName: 'John' },
});Aggregate context from subject hierarchy:
const aggregated = await b.aggregate(ctx, {
subject: 'eventInstance',
subjectId: 'ei_123',
});
// Returns variables from eventInstance + parent event + associated beneficiaryBind subject types to your host tables for automatic context resolution:
const b = bridge.create(components.bridge, {
subjects: {
beneficiary: { table: 'beneficiaries' },
event: { table: 'events' },
eventInstance: {
table: 'eventInstances',
parents: [{ field: 'eventId', subject: 'event' }],
},
},
});When subjects are bound, Bridge can automatically fetch subject data when evaluating deliverables.
Important: Component hooks receive generic context types that may not be compatible with your host app's schema. Handle authorization in wrapper functions where you have properly typed context:
// convex/procedures.ts - Authorization at wrapper level
import { query, mutation } from './_generated/server';
import { components } from './_generated/api';
import { verifyOrgAccess } from './permissions';
export const procedureGet = query({
args: { id: v.string() },
handler: async (ctx, { id }) => {
const procedure = await ctx.runQuery(components.bridge.public.procedureGet, { id });
if (procedure) {
// Verify with properly typed context from your schema
await verifyOrgAccess(ctx, procedure.organizationId);
}
return procedure;
},
});All validators and types are consolidated in a single source of truth:
import {
// Enums with display names
CardType,
SecurityLevel,
ProcedureType,
Operation,
EvaluationStatus,
DeliverableStatus,
// Validators
cardTypeValidator,
securityLevelValidator,
procedureTypeValidator,
operationValidator,
evaluationStatusValidator,
deliverableStatusValidator,
// Types (derived from validators)
type Card,
type Procedure,
type Deliverable,
type Evaluation,
type ProcedureCard,
type DeliverableOperation,
type EvaluationContext,
type EvaluationResult,
// Branded IDs
type CardId,
type ProcedureId,
type DeliverableId,
type EvaluationId,
type OrganizationId,
// ID factory
createId,
// Duration utilities
type Duration, // "30s" | "5m" | "2h" | "7d"
parseDuration,
formatDuration,
} from '@trestleinc/bridge';import {
bridge, // Factory to create bridge instance
clientApi, // Alternative API factory
createTriggers, // Generate Convex trigger handlers
createSubjectTrigger, // Single trigger handler
extractAttributeChanges, // Detect changed attributes
// Error types
BridgeError,
NotFoundError,
ValidationError,
AuthorizationError,
ConflictError,
} from '@trestleinc/bridge/server';import {
// Error types
NetworkError,
AuthorizationError,
NotFoundError,
ValidationError,
NonRetriableError,
} from '@trestleinc/bridge/client';All logging is unified in shared/ using LogTape with ANSI colored console output:
import { getLogger } from '$/shared/logger';
// Get a LogTape logger with category
const logger = getLogger(['bridge', 'cards']);
logger.info('Card created', { cardId: 'card_123' });
logger.debug('Processing request', { procedureId });
logger.error('Failed to evaluate', { error });
// Categories help filter and organize log output
// ANSI colored output is configured automaticallysrc/
├── shared/
│ ├── index.ts # All validators, types, branded IDs, enums, utilities
│ └── logger.ts # Unified LogTape logger (ANSI colored console output)
├── client/
│ └── index.ts # Error types
├── server/
│ ├── index.ts # Factory function, triggers, error types
│ └── resources/ # Resource implementations (cards, procedures, etc.)
└── component/
└── ... # Convex component internals
Bridge follows a consistent API design pattern across all entry points:
1. Factory Pattern - Create configured instances:
import { bridge } from '@trestleinc/bridge/server';
// Factory creates a configured bridge instance
const b = bridge.create(components.bridge, {
subjects: {
beneficiary: { table: 'beneficiaries' },
event: { table: 'events' },
},
});2. Namespace Pattern - Organized resource access:
// Resources are organized under namespaces
b.cards.get; // Get a card
b.cards.list; // List cards
b.procedures.create; // Create a procedure
b.deliverables.evaluate; // Evaluate deliverables
b.evaluations.complete; // Complete an evaluation3. Getter Pattern - Direct property access for resource methods:
// Each method returns a Convex function reference
export const get = b.cards.get; // Re-export as Convex query
export const list = b.cards.list; // Re-export as Convex query
```typescript
// Resources are organized under namespaces
b.cards.get // Get a card
b.cards.list // List cards
b.procedures.create // Create a procedure
b.deliverables.evaluate // Evaluate deliverables
b.evaluations.complete // Complete an evaluation3. Getter Pattern - Direct property access for resource methods:
// Each method returns a Convex function reference
export const get = b.cards.get; // Re-export as Convex query
export const list = b.cards.list; // Re-export as Convex query
export const create = b.cards.create; // Re-export as Convex mutation4. Single Entry Point - All exports consolidated per layer:
// Server - everything from one import
import { bridge, BridgeError, NotFoundError, createTriggers } from '@trestleinc/bridge/server';
// Client - error types from one import
import { NetworkError, ValidationError } from '@trestleinc/bridge/client';
// Logger - unified in shared
import { getLogger } from '$/shared/logger';
// Shared - all validators and types from one import (default export)
import {
Card,
Procedure,
Deliverable,
Evaluation,
CardType,
SecurityLevel,
ProcedureType,
cardDocValidator,
procedureDocValidator,
createId,
parseDuration,
Card,
Procedure,
Deliverable,
Evaluation,
CardType,
SecurityLevel,
ProcedureType,
cardDocValidator,
procedureDocValidator,
createId,
parseDuration,
} from '@trestleinc/bridge';Apache-2.0