[Epic]: UDL as Production Data Layer
Vision Statement
Deploy UDL as a durable, webhook-driven data cache that build workers and production apps consume, instead of each consumer directly hitting external APIs.
Primary function: Production data layer that build workers and apps query via GraphQL
Secondary function: WebSocket relay for local dev machines to receive real-time updates (opt-in)
External APIs (Contentful, Shopify, etc.)
│
│ N webhooks (e.g., 30 entry publishes)
▼
┌─────────────────────────────────────────────┐
│ Remote UDL │
│ ┌───────────────────────────────────────┐ │
│ │ Webhook Queue + Debounce (Xs wait) │ │
│ └───────────────────────────────────────┘ │
│ │ │
│ ▼ (after quiet period) │
│ ┌───────────────────────────────────────┐ │
│ │ Node Store + Deletion Log (30d TTL) │ │
│ └───────────────────────────────────────┘ │
│ │ │ │ │
│ GraphQL API WebSocket Outbound │
│ (opt-in) Webhook │
└─────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
Build workers Local devs Vercel/CI
ISR/SSG/Apps (real-time) (1 rebuild)
Why This Matters
- Performance: One UDL instance handles external API calls, not N consumers
- Consistency: Single source of truth for all data consumers
- Real-time: Webhooks keep data current; WebSocket relays to local devs
- Smart Builds: Webhook batching prevents 30 publishes = 30 builds (→ 1 build)
- Partial Sync: Deletion log enables catch-up without full refetch
Key Design Decisions
- GraphQL queries for consumers (no WebSocket needed for reads)
- Webhook queue + debounce prevents N webhooks → N builds
- WebSocket for local dev sync (opt-in, relay changes to connected clients)
- Deletion log (30-day) enables partial sync without full refetch
- Pluggable cache storage already exists
- All config lives in
udl.config.ts under remote field
Config API
export const config = defineConfig({
port: 4000,
plugins: [ ... ],
codegen: { ... },
remote: {
url: 'https://udl.example.com/graphql', // Remote UDL URL (for local instances)
websockets: true, // Enable subscriptions over WebSocket
webhooks: {
listen: true, // Enable listening at /webhooks
trigger: [ // Outgoing webhooks (for rebuilds)
{
url: 'https://my-webhook-endpoint.com/udl',
events: ['*'], // Which events ('*' = all)
batch: true, // Enable batching
debounceMs: 1000, // Wait 1s for more webhooks
}
]
}
}
});
What's Already Built
| Component |
Status |
| Pluggable Cache Storage |
✅ Ready |
| Node Store with indexes |
✅ Ready |
| GraphQL API |
✅ Ready |
| Hot Reload (dev mode) |
✅ Ready |
| Plugin Lifecycle hooks |
✅ Ready |
What's Missing
| Feature |
Priority |
Purpose |
| Webhook Infrastructure |
CRITICAL |
Receive updates from data sources |
| Webhook Queue & Debounce |
CRITICAL |
Batch incoming webhooks, trigger ONE outbound rebuild |
| WebSocket Server |
HIGH |
Relay changes to local dev machines |
| Deletion Log |
HIGH |
Enable partial sync (30-day TTL) |
| Production Hardening |
HIGH |
Health checks, graceful shutdown |
| Deployment Guide |
MEDIUM |
Docs for running in production |
Implementation Phases
Phase 1: Production Server Hardening
- Health check endpoints (
/health, /ready)
- Graceful shutdown (SIGTERM/SIGINT handlers)
- Request logging (optional)
Files:
packages/core/src/server.ts
packages/core/src/start-server.ts
Phase 2: Webhook Infrastructure
- Plugin webhook registration in
SourceNodesContext
- Webhook routing (
POST /_webhooks/:plugin/:handler)
- Signature verification utilities
Files:
packages/core/src/loader.ts - add webhook registry
packages/core/src/server.ts - add webhook routes
packages/core/src/nodes/index.ts - extend context
API:
// In plugin's sourceNodes hook
registerWebhook({
path: 'contentful-update',
handler: async (req, res) => { /* process, update nodes */ },
verifySignature: (req) => verifyContentfulSignature(req, secret),
});
Phase 3: Webhook Queue & Debounce (Build Trigger Optimization)
- Queue incoming webhooks instead of immediate processing
- Configurable debounce window (e.g., 5 seconds)
- After quiet period, process batch and trigger ONE outbound webhook
- Outbound webhook URL configurable per environment
Flow:
Webhook 1 arrives → start timer (5s)
Webhook 2 arrives → reset timer (5s)
Webhook 3 arrives → reset timer (5s)
... 5 seconds of quiet ...
→ Process all queued webhooks (update nodes)
→ Trigger ONE outbound webhook (rebuild)
→ Broadcast changes via WebSocket to local devs
Phase 4: Deletion Log
- Track node deletions with timestamp
- 30-day TTL (auto-cleanup)
- Query API: "what was deleted since X?"
Storage: Extend cache storage interface or separate log file
Phase 5: WebSocket Server (opt-in)
- WebSocket upgrade on
/ws or /_sync
- Broadcast events on node changes:
node:created, node:updated, node:deleted
- Initial sync endpoint:
GET /_sync?since={ISO8601}
Protocol:
// Server broadcasts after webhook processing
ws.send(JSON.stringify({
type: 'node:updated',
nodeId: 'Product-123',
timestamp: '2025-12-21T10:30:00Z',
data: { /* full node or diff */ }
}));
Phase 6: Documentation
- Production deployment guide (Vercel, Railway, Docker, VPS)
- Plugin webhook implementation guide
- Local sync setup guide
Success Criteria
Out of Scope (Future)
- Custom cache backends (Redis, S3) - interface exists, implementations later
- Auth/rate limiting - handle externally (nginx, Cloudflare)
- Multi-region federation
Related Work
Linked Issues
Phase 1: Production Server Hardening
Phase 2: Webhook Infrastructure
Phase 3: Webhook Queue & Debounce
Phase 4: Deletion Log
Phase 5: WebSocket Server
Phase 6: Documentation
Dependency Graph
Phase 1: Production Hardening
#58 (Health checks)
│
#59 (Graceful shutdown) ← depends on #58
Phase 2: Webhook Infrastructure
#60 (Webhook registration API)
│
#61 (Webhook routing) ← depends on #60
Phase 3: Webhook Queue & Debounce
#62 (Webhook queue) ← depends on #61
│
#63 (Outbound webhooks) ← depends on #62
Phase 4: Deletion Log (can run parallel with Phase 2-3)
#64 (Deletion log)
│
#65 (Sync API) ← depends on #64
Phase 5: WebSocket Server
#66 (WebSocket server) ← depends on #62
Phase 6: Documentation (after implementation)
#67, #68, #69 ← depends on respective features
[Epic]: UDL as Production Data Layer
Vision Statement
Deploy UDL as a durable, webhook-driven data cache that build workers and production apps consume, instead of each consumer directly hitting external APIs.
Primary function: Production data layer that build workers and apps query via GraphQL
Secondary function: WebSocket relay for local dev machines to receive real-time updates (opt-in)
Why This Matters
Key Design Decisions
udl.config.tsunderremotefieldConfig API
What's Already Built
What's Missing
Implementation Phases
Phase 1: Production Server Hardening
/health,/ready)Files:
packages/core/src/server.tspackages/core/src/start-server.tsPhase 2: Webhook Infrastructure
SourceNodesContextPOST /_webhooks/:plugin/:handler)Files:
packages/core/src/loader.ts- add webhook registrypackages/core/src/server.ts- add webhook routespackages/core/src/nodes/index.ts- extend contextAPI:
Phase 3: Webhook Queue & Debounce (Build Trigger Optimization)
Flow:
Phase 4: Deletion Log
Storage: Extend cache storage interface or separate log file
Phase 5: WebSocket Server (opt-in)
/wsor/_syncnode:created,node:updated,node:deletedGET /_sync?since={ISO8601}Protocol:
Phase 6: Documentation
Success Criteria
Out of Scope (Future)
Related Work
Linked Issues
Phase 1: Production Server Hardening
Phase 2: Webhook Infrastructure
Phase 3: Webhook Queue & Debounce
Phase 4: Deletion Log
Phase 5: WebSocket Server
Phase 6: Documentation
Dependency Graph