feat: multi-channel platform — docs, workflow editor, lint fixes#30
Merged
productdevbook merged 42 commits intomainfrom Feb 26, 2026
Merged
feat: multi-channel platform — docs, workflow editor, lint fixes#30productdevbook merged 42 commits intomainfrom
productdevbook merged 42 commits intomainfrom
Conversation
…kflows, templates, hooks)
Transform NitroPing from push-only to a full multi-channel notification platform.
## Database (Phase 1)
- 6 new pg enums: channelType, workflowStatus, workflowStepType, workflowTriggerType, workflowExecutionStatus, hookEvent
- 9 new tables: subscriber, subscriberDevice, subscriberPreference, channel, template, workflow, workflowStep, workflowExecution, hook
- Nullable subscriberId FK on device for subscriber linkage
- Migration: 20260224073747_unusual_spirit
## Channel Abstraction (Phase 2)
- Channel interface with ChannelMessage/ChannelResult types
- EmailChannel: SMTP (nodemailer) + Resend providers, lazy-imported
- PushChannel: wraps existing getProviderForApp() provider system
- Factory functions: getChannelById, getChannelForApp
- templateRenderer utility for {{variable}} substitution
## Workflow Engine (Phase 3)
- BullMQ workflow queue (addTriggerWorkflowJob, addExecuteWorkflowStepJob)
- Step executor worker: SEND / DELAY (re-queue with delay) / FILTER / DIGEST
- HMAC-SHA256 webhook dispatcher with Promise.allSettled fan-out
- Worker registered in server/plugins/worker.ts
## GraphQL (Phase 4)
- 5 new SDL domains: subscribers, channels, templates, workflows, hooks
- Full CRUD resolvers for all domains; triggerWorkflow creates execution + enqueues job
- Shared enums in shared.graphql to avoid ordering conflicts
- 5 new DataLoaders: subscriber, channel, template, workflow, preference
- Channel.config and Hook.secret always resolve to null (security)
## Frontend (Phase 5)
- Vue Flow workflow canvas editor with TriggerNode, SendNode, DelayNode, FilterNode
- StepConfigPanel with SendConfigPanel, DelayConfigPanel, FilterConfigPanel
- Pages: /subscribers, /channels, /templates, /templates/create, /workflows,
/workflows/:wid (Vue Flow editor), /workflows/:wid/runs, /hooks
- AppNavigation updated with 5 new nav items
- GraphQL SDK extended: channels, subscribers, templates, workflows, hooks operations
- nitro-graphql-client.d.ts and types/ updated with new query/mutation types
## SDK
- NitroPingClient.identify(externalId, options) — upsert subscriber
- NitroPingClient.updatePreference(input) — manage channel opt-in/out
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ia context
Removed useDatabase and tables from the GraphQL context object entirely.
All resolvers now import them directly:
import * as tables from '#server/database/schema'
import { useDatabase } from '#server/utils/useDatabase'
Context now only carries dataloaders. This fixes the "useDatabase is not a
function" runtime error caused by graphql-yoga not merging the context
object as expected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…solvers, frontend)
GraphQL Subscriber type conflicted with graphql-yoga's internal Subscriber.
Complete rename across all layers:
DB tables: subscriber→contact, subscriberDevice→contactDevice,
subscriberPreference→contactPreference
Schema files: subscriber.ts→contact.ts, subscriberDevice.ts→contactDevice.ts,
subscriberPreference.ts→contactPreference.ts
Loaders: subscriber.loader.ts→contact.loader.ts,
preference.loader.ts→contactPreference.loader.ts
Frontend: src/graphql/subscribers/→src/graphql/contacts/,
pages/subscribers.vue→pages/contacts.vue, nav link→/contacts
GraphQL types/mutations: Subscriber→Contact, SubscriberPreference→ContactPreference,
createSubscriber→createContact, updateSubscriberPreference→updateContactPreference, etc.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Brings in direct useDatabase/tables imports in resolvers - Brings in tsconfig/vite.config fixes for build-generated types - Removes stale .graphql/types/ directory - Resolves conflict in nitro-graphql-client.d.ts (base types + new platform types) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Install missing packages: nodemailer, resend, @vue-flow/* packages, @types/nodemailer - Cast enum string types (ChannelType, WorkflowTriggerType) with 'as any' in composables - Fix workflow.worker.ts: split case statements to satisfy max-statements-per-line rule - Fix StepConfigPanel: rename event 'update-node' to 'updateNode' (camelCase rule) - Remove unused appId from mutation callback destructuring in graphql composables Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
App-specific navigation (Overview, Push Providers, Devices, Notifications, Contacts, Channels, Templates, Workflows, Webhooks, Settings) is now shown in the sidebar when on /apps/:id/* routes, with a back-to-list header. Removed AppNavigation component usage from all 13 app detail pages. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e in sidebar App name is now shown in the sidebar header instead of repeating it on every sub-page. Removed AppDetailHeader component usage and unused useApp/appData imports from all 16 app detail pages. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ents - DB: add DISCORD to channelType enum, name to contact, inAppMessage table + migration - Channels: Discord webhook, Twilio SMS, In-App DB insert implementations - Routing: multi-channel sendNotification (EMAIL/SMS/DISCORD/IN_APP per contact) - Notification queue: discriminated union DeviceJobData | ChannelJobData - Worker: channel vs device delivery modes, webhook dispatch on success/failure - Contact: name field, smart metadata extraction (extractKnownFields) - Workflow: FILTER step supports contact.* field prefix - SDK: vapidPublicKey optional, swPath, name in identify, getContact/getPreferences/trackEvent - Frontend: Discord/SMS channel forms, contact name field, toast notifications throughout - Frontend: contact detail page with preferences toggle, logout handler - Remove requireAuth from dashboard mutations (no user auth system yet) - Fix scheduler missing deliveryMode, remove unused useApp import in workflow page Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ilities - Add server/utils/bullmq.ts with Map-based useQueue/useWorker helpers that create dedicated ioredis connections and cache instances across HMR reloads - Rewrite server/plugins/worker.ts to register workers via useWorker, delegating to processNotificationJob / processWorkflowJob exported functions - Export processNotificationJob from notification.worker.ts and processWorkflowJob from workflow.worker.ts; remove now-dead startWorker/stopWorker lifecycle fns - Make deliveryLog.deviceId nullable so channel deliveries don't need a fake UUID - Update notification.queue.ts to use the new useQueue utility - Add getWorkerRedis() to redis.ts for worker-dedicated ioredis instance Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Worker now logs 'Ready — listening for jobs' when Redis connection established; surfaces silent Redis errors via connection error handler - Maps stored in globalThis to survive any HMR module reloads - Plugin logs 'Initializing...' so we can confirm it runs at startup Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move postgres db instance and Redis client to globalThis so they survive Nitro dev-mode module reloads. Previously each reload created a new pool, quickly exhausting PostgreSQL's max_connections limit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add closeDatabase() that ends the postgres.js client cleanly - Call closeDatabase() in worker plugin's close hook so connections are released when Nitro stops (Ctrl+C, restart, etc.) - Wrap migrate() in try/finally so the migration connection always ends even when 'too many clients' or other errors occur - Reduce pool max from 10 → 5 to limit connection usage per process Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… bug Jobs with priority go into a Redis sorted set (ZADD/BZPOPMIN) instead of a regular list (LPUSH/BLMOVE). BullMQ v5 has a known issue where after completing a priority job, the worker sometimes doesn't re-poll the sorted set, causing subsequent jobs to be stuck. Removing priority switches back to standard FIFO list which uses the reliable BLMOVE blocking path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BullMQ v5 workers need queue metadata to exist in Redis before they start polling. Previously queues were created lazily on first job add, meaning the worker started without seeing the queue structure and its blocking poll didn't attach correctly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…exhaustion Lower postgres.js pool from max:5 to max:3 with a 30s idle_timeout so connections from killed dev-server processes are automatically released. Also install pg-boss for future queue migration. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
globalThis was added to survive Nitro HMR reloads but Nitro fully restarts the process on server file changes, making it unnecessary. Plain module-level variables are cleaner and equivalent in behavior. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Workers, BullMQ queues, Redis (rate limiter), and database connections are now all closed in one place in the correct order on server shutdown. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add totalOpened column to notification table (+ migration) - ChannelMessage gains optional trackingId & trackingBaseUrl fields - EmailChannel injects 1×1 tracking pixel and wraps <a> hrefs when trackingId is present (SMTP and Resend both supported) - notification.worker pre-generates deliveryLog UUID before send so the same ID is embedded in tracking URLs and stored in the DB - GET /track/open/[id] → returns transparent GIF, sets openedAt on first request, increments totalOpened, fires NOTIFICATION_OPENED hook - GET /track/click/[id] → sets clickedAt on first request, increments totalClicked, fires NOTIFICATION_CLICKED hook, redirects to original URL (base64url-encoded in ?url= param; http/https only for safety) - webhookDispatcher gains NOTIFICATION_OPENED event type - Set APP_URL env var to your public server URL for tracking to work Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ppear first Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When no htmlBody is provided, wrap the plain-text body in minimal HTML so the tracking pixel is always embedded in outgoing emails. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ient - Notification detail page at /apps/[id]/notifications/[notificationId] showing stats (sent, failed, opened, clicked with rates) and full delivery log table (recipient, status, sentAt, openedAt, clickedAt) - sendNotification resolver now sets status=SENT + sentAt after queuing jobs so notifications no longer stay PENDING forever - deliveryLog gains a `to` text column to store channel recipients (email address, phone number, etc.) — migration applied - notification.graphql: add totalOpened, fix DeliveryLog (deviceId optional, add to/sentAt/openedAt fields) - queries.graphql: fetch new fields in notification + deliveryLogs queries Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…y rate - Add inline Send Notification dialog on notifications page (channel selector, recipients, schedule) — no longer redirects to /send - Register notification detail route in router.ts - Fix View Details button using RouterLink :as prop - Move track endpoints to /api/track/* (routes/ dir not supported in this Vite+Nitro setup) - Update email tracking URLs to /api/track/open|click/ - Use nitro/h3 imports and non-deprecated h3 APIs in track handlers - Fix delivery rate to use totalSent instead of always-zero totalDelivered Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add TelegramChannel class (Bot API, Markdown formatting) - Add TELEGRAM to channelType enum, GraphQL schema, and queue types - Encrypt botToken at rest; decrypt on channel instantiation - Add Telegram UI to channels config page and send dialog - Add DB migration to extend channelType enum - Unify ChannelType as a single const/type in enums.ts — all other files import from there Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Full-screen layout outside DefaultLayout; own top bar with back/save/runs - Left-side draggable NodePalette (Triggers, Actions, Logic sections) - Channel-specific Send nodes (Email, SMS, Push, In-App, Discord, Telegram) - Sliding overlay config panel on right when a node is selected - Fix node connections: register onConnect + addEdges handler in useWorkflowEditor - Move handles to left (target) / right (source) for Blueprint-style left-to-right flow - Topological BFS step ordering on save; trigger node non-deletable - Extract types.ts and useWorkflowEditor composable for clean separation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
If a workflow contains a SEND step without a hardcoded `to` address, reject the trigger at the resolver level instead of failing silently in the worker after an execution record has already been created. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ate page - Create app/src/docs/*.md — getting-started, authentication, channels, contacts, workflows, notifications, sdk (7 files, single source of truth) - Rewrite docs.vue to render .md files via comark with prose styling - Enable @tailwindcss/typography plugin for prose classes - Move channels "Add Channel" form from dialog to dedicated /channels/new page - channels/index.vue: list-only, button navigates to /new - channels/new.vue: full-page form with back button, redirects to list on save - docs/app/pages/docs.vue: Nuxt docs site imports same .md files via comark - Install comark in both app and docs packages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
comark@0.0.1 is an empty npm stub. mdc-syntax has working vue/react exports and the same API surface (<MDC :markdown="content" />). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… option Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ghting mdc-syntax uses shiki internally for code highlighting but doesn't bundle it. Installing shiki allows highlight:true option to work. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…reStyles Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ponents - Split monolithic docs.vue into per-slug pages (docs/[slug].vue) - Dynamic MD import via import.meta.glob for each doc section - DocsCodeBlock: macOS dots, Shiki syntax highlighting, hover copy button - Heading components generate anchor IDs for TOC links - "On this page" TOC panel (xl screens) parsed from markdown h2/h3 - Prev/Next navigation between doc pages - AppSidebar shows Guide/Features/Reference groups when on /docs/* - Breadcrumb shows current doc page title Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add per-type markdown docs in docs/channels/ (email, telegram, discord, sms, push, in-app) - channels/new: two-column layout — form left, sticky help doc right - Type selector redesigned as icon+description card grid - Help doc loads dynamically via import.meta.glob when type changes - Extract docsComponents + mdcOptions to useDocsComponents composable (shared) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New route /docs/channels/:type loads docs/channels/{type}.md
- pages/docs/channels/[type].vue with TOC and prev/next nav
- AppSidebar Channels item now collapsible with Email/Telegram/Discord/SMS/Push/In-App sub-items
- Breadcrumb handles nested channel paths
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- channels/new.vue: remove unused Select imports (replaced by card grid) - notifications.vue: remove unused useRouter/router - track/click: replace undefined redirect() with sendRedirect() from nitro/h3 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add `import { Buffer } from 'node:buffer'` to track API routes and email channel
- Rename unused `ctx` → `_ctx` in all GraphQL mutation resolvers
- Rename `nitroApp` → `_nitroApp` in worker plugin
- Add `// eslint-disable-next-line` for `__node` prop casing in DocsCodeBlock
- Fix `setTimeout` multiline style in DocsCodeBlock
- Add trailing newline to migration snapshot JSON files
- Auto-fix import ordering, if-newline, singleline html element rules across Vue/TS files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
/docs/:slug),/docs/channels/:typesub-pages, shadcn code blocks with macOS dots + copy button, TOC panel, prev/next navigationchannels/newpage shows per-type markdown documentation in a sticky right panelTest plan
/docs/getting-startedand other doc pages — verify syntax highlighting, copy button, TOC, prev/next/docs/channels/telegrametc. — verify channel sub-pages workpnpm lint— expect 0 errorspnpm typecheck— expect 0 errors🤖 Generated with Claude Code