Skip to content

feat: multi-channel platform — docs, workflow editor, lint fixes#30

Merged
productdevbook merged 42 commits intomainfrom
feat/multi-channel-platform
Feb 26, 2026
Merged

feat: multi-channel platform — docs, workflow editor, lint fixes#30
productdevbook merged 42 commits intomainfrom
feat/multi-channel-platform

Conversation

@productdevbook
Copy link
Owner

Summary

  • Multi-channel platform: Discord, SMS (Twilio), In-App, Telegram channel support with contact management and workflow orchestration
  • Free-form workflow editor: n8n-style node canvas with draggable palette, manual connections, channel-specific action nodes
  • VitePress-style docs: Per-slug pages (/docs/:slug), /docs/channels/:type sub-pages, shadcn code blocks with macOS dots + copy button, TOC panel, prev/next navigation
  • Inline channel setup guides: channels/new page shows per-type markdown documentation in a sticky right panel
  • Notification tracking: Email open/click tracking with pixel injection and link wrapping
  • Lint clean: 0 ESLint errors — fixed Buffer imports, unused ctx args, import ordering, if-newline, migration snapshot EOL

Test plan

  • Create channels of each type (Email, Telegram, Discord, SMS, Push, In-App) — verify inline setup guide loads per type
  • Open /docs/getting-started and other doc pages — verify syntax highlighting, copy button, TOC, prev/next
  • Open /docs/channels/telegram etc. — verify channel sub-pages work
  • Open workflow editor — drag nodes from palette onto canvas, connect with edges, save and verify topological order
  • Send notification via email channel — verify open/click tracking pixel and redirect
  • Run pnpm lint — expect 0 errors
  • Run pnpm typecheck — expect 0 errors

🤖 Generated with Claude Code

productdevbook and others added 30 commits February 24, 2026 10:49
…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>
productdevbook and others added 12 commits February 26, 2026 20:09
…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>
@productdevbook productdevbook merged commit feafd04 into main Feb 26, 2026
1 check passed
@productdevbook productdevbook deleted the feat/multi-channel-platform branch February 26, 2026 21:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant