From 9355c0410fdeaeb322809b0af10066ef1c49d06d Mon Sep 17 00:00:00 2001 From: ephraimduncan Date: Mon, 26 Jan 2026 21:04:25 +0000 Subject: [PATCH 1/2] feat: add webhook queue system and worker - Add queue package with BullMQ for webhook delivery - Add worker app to process webhook jobs - Add webhook delivery logs schema and tracking - Update UI components for Tailwind v4 - Add .env.example, improve API and tests --- .../.env.development.example => .env.example | 5 +- .eslintrc.cjs | 8 +- apps/web/next.config.js | 2 +- apps/web/package.json | 8 +- apps/web/src/app/(auth)/login/login.tsx | 4 +- .../(auth)/reset-password/[token]/page.tsx | 1 - .../src/app/(auth)/reset-password/page.tsx | 1 - .../reset-password/send-reset-email.tsx | 9 +- .../app/(auth)/verify-email/verify-code.tsx | 4 +- .../(main)/dashboard/_components/forms.tsx | 4 +- .../dashboard/_components/new-form-dialog.tsx | 3 +- .../settings/api-keys/api-keys-section.tsx | 9 +- .../dashboard/settings/profile-form.tsx | 3 +- .../app/(main)/form/[id]/form-settings.tsx | 187 +++- apps/web/src/app/(main)/form/[id]/page.tsx | 7 +- .../(main)/form/[id]/submissions-table.tsx | 22 +- .../onboarding/form/create-form-dialog.tsx | 3 +- apps/web/src/app/api/auth/[...all]/route.ts | 3 +- .../app/api/cron/cleanup-audit-logs/route.ts | 2 +- apps/web/src/app/api/s/[id]/route.ts | 39 +- apps/web/src/app/api/v1/[...trpc]/route.ts | 13 +- apps/web/src/app/api/v1/openapi.json/route.ts | 2 +- apps/web/src/app/icon.tsx | 20 +- apps/web/src/components/password-input.tsx | 4 +- apps/web/src/components/user-avatar.tsx | 4 +- apps/web/src/lib/hooks/use-social-auth.ts | 6 +- apps/web/src/lib/trpc/react.tsx | 2 +- apps/web/src/styles/globals.css | 72 +- apps/web/tailwind.config.ts | 3 +- apps/worker/package.json | 23 + apps/worker/src/index.ts | 29 + apps/worker/tsconfig.json | 7 + bun.lock | 84 ++ docker/docker-compose.yml | 10 + package.json | 15 +- packages/api/lib/api-key.ts | 6 +- packages/api/lib/audit-log.ts | 12 +- packages/api/middleware/api-auth.ts | 4 +- packages/api/middleware/rate-limit.ts | 4 +- packages/api/package.json | 1 + packages/api/routers/api-keys.ts | 10 +- packages/api/routers/api-v1/index.ts | 4 +- packages/api/routers/api-v1/ownership.ts | 11 +- packages/api/routers/api-v1/submissions.ts | 17 +- packages/api/routers/api-v1/trpc.ts | 5 +- packages/api/routers/form.ts | 73 +- packages/api/routers/formData.ts | 12 +- packages/api/utils/json.ts | 4 +- packages/config/tailwind/src/preset.ts | 25 +- packages/db/drizzle/0003_serious_the_fury.sql | 23 + packages/db/drizzle/meta/0000_snapshot.json | 95 +- packages/db/drizzle/meta/0001_snapshot.json | 135 +-- packages/db/drizzle/meta/0002_snapshot.json | 135 +-- packages/db/drizzle/meta/0003_snapshot.json | 979 ++++++++++++++++++ packages/db/drizzle/meta/_journal.json | 9 +- packages/db/index.ts | 14 +- packages/db/migrate.ts | 9 +- packages/db/package.json | 16 +- packages/db/schema/api-audit-logs.ts | 2 +- packages/db/schema/api-keys.ts | 2 +- packages/db/schema/form-data.ts | 2 +- packages/db/schema/forms.ts | 8 +- packages/db/schema/index.ts | 1 + packages/db/schema/relations.ts | 19 +- packages/db/schema/users.ts | 2 +- packages/db/schema/webhook-delivery-logs.ts | 48 + packages/email/index.ts | 5 +- packages/email/package.json | 1 - packages/email/tsconfig.json | 1 - packages/env/index.ts | 2 + packages/queue/connection.ts | 23 + packages/queue/index.ts | 10 + packages/queue/package.json | 29 + packages/queue/queues/webhook.ts | 40 + packages/queue/services/delivery.ts | 237 +++++ packages/queue/tsconfig.json | 7 + packages/queue/workers/webhook.ts | 75 ++ packages/ui/primitives/accordion.tsx | 40 +- packages/ui/primitives/alert.tsx | 59 +- packages/ui/primitives/aspect-ratio.tsx | 12 +- packages/ui/primitives/avatar.tsx | 64 +- packages/ui/primitives/badge.tsx | 50 +- packages/ui/primitives/breadcrumb.tsx | 73 +- packages/ui/primitives/button-group.tsx | 50 +- packages/ui/primitives/calendar.tsx | 174 ++-- packages/ui/primitives/card.tsx | 63 +- packages/ui/primitives/carousel.tsx | 197 ++-- packages/ui/primitives/chart.tsx | 211 ++-- packages/ui/primitives/checkbox.tsx | 19 +- packages/ui/primitives/collapsible.tsx | 12 +- packages/ui/primitives/combobox.tsx | 103 +- packages/ui/primitives/command.tsx | 83 +- packages/ui/primitives/context-menu.tsx | 94 +- packages/ui/primitives/dialog.tsx | 74 +- packages/ui/primitives/drawer.tsx | 55 +- packages/ui/primitives/dropdown-menu.tsx | 113 +- packages/ui/primitives/empty.tsx | 63 +- packages/ui/primitives/field.tsx | 150 +-- packages/ui/primitives/form.tsx | 5 +- packages/ui/primitives/hover-card.tsx | 24 +- packages/ui/primitives/input-group.tsx | 110 +- packages/ui/primitives/input-otp.tsx | 56 +- packages/ui/primitives/input.tsx | 23 +- packages/ui/primitives/item.tsx | 132 +-- packages/ui/primitives/kbd.tsx | 16 +- packages/ui/primitives/label.tsx | 16 +- packages/ui/primitives/menubar.tsx | 108 +- packages/ui/primitives/navigation-menu.tsx | 70 +- packages/ui/primitives/pagination.tsx | 69 +- packages/ui/primitives/popover.tsx | 47 +- packages/ui/primitives/progress.tsx | 33 +- packages/ui/primitives/radio-group.tsx | 22 +- packages/ui/primitives/resizable.tsx | 27 +- packages/ui/primitives/scroll-area.tsx | 23 +- packages/ui/primitives/select.tsx | 85 +- packages/ui/primitives/separator.tsx | 16 +- packages/ui/primitives/sheet.tsx | 66 +- packages/ui/primitives/sidebar.tsx | 463 +++++---- packages/ui/primitives/skeleton.tsx | 10 +- packages/ui/primitives/slider.tsx | 21 +- packages/ui/primitives/sonner.tsx | 57 +- packages/ui/primitives/spinner.tsx | 18 +- packages/ui/primitives/switch.tsx | 18 +- packages/ui/primitives/table.tsx | 73 +- packages/ui/primitives/tabs.tsx | 50 +- packages/ui/primitives/textarea.tsx | 14 +- packages/ui/primitives/toggle-group.tsx | 55 +- packages/ui/primitives/toggle.tsx | 36 +- packages/ui/primitives/tooltip.tsx | 26 +- packages/ui/primitives/typography.tsx | 2 +- tests/api/api-v1.test.ts | 26 +- tests/api/form.test.ts | 4 +- tests/api/formData.test.ts | 5 +- tests/api/security.test.ts | 20 +- tests/api/user.test.ts | 4 +- tests/helpers/api-v1.ts | 3 +- tests/helpers/db.ts | 7 +- tests/helpers/factories.ts | 2 +- tests/package.json | 24 +- tests/routes/submission.test.ts | 4 +- tests/vitest.config.ts | 9 +- turbo.json | 5 +- 142 files changed, 4207 insertions(+), 2191 deletions(-) rename apps/web/.env.development.example => .env.example (87%) create mode 100644 apps/worker/package.json create mode 100644 apps/worker/src/index.ts create mode 100644 apps/worker/tsconfig.json create mode 100644 packages/db/drizzle/0003_serious_the_fury.sql create mode 100644 packages/db/drizzle/meta/0003_snapshot.json create mode 100644 packages/db/schema/webhook-delivery-logs.ts create mode 100644 packages/queue/connection.ts create mode 100644 packages/queue/index.ts create mode 100644 packages/queue/package.json create mode 100644 packages/queue/queues/webhook.ts create mode 100644 packages/queue/services/delivery.ts create mode 100644 packages/queue/tsconfig.json create mode 100644 packages/queue/workers/webhook.ts diff --git a/apps/web/.env.development.example b/.env.example similarity index 87% rename from apps/web/.env.development.example rename to .env.example index a1c9eb0..a616a61 100644 --- a/apps/web/.env.development.example +++ b/.env.example @@ -1,5 +1,5 @@ # Development environment (.env.local) -# Copy this file to apps/web/.env.local +# Copy this file to .env.local in the root directory # Drizzle / libSQL (local sqlite) DATABASE_URL=file:./local.db @@ -36,3 +36,6 @@ STORAGE_USESSL=false STORAGE_ACCESS_KEY=formbase STORAGE_SECRET_KEY=password STORAGE_BUCKET=formbase + +# Redis (for webhook queue) +REDIS_URL=redis://localhost:6379 diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 7e59b46..0636f62 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,6 +1,12 @@ /** @type {import('eslint').Linter.Config} */ const config = { - ignorePatterns: ['apps/**', 'packages/**', 'tests/playwright-report/**', 'tests/test-results/**', '**/.eslintrc.cjs'], + ignorePatterns: [ + 'apps/**', + 'packages/**', + 'tests/playwright-report/**', + 'tests/test-results/**', + '**/.eslintrc.cjs', + ], extends: ['formbase/base'], }; diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 07ce97b..de0d631 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -20,7 +20,7 @@ const nextConfig = { '@formbase/env', '@formbase/ui', '@formbase/utils', - "@formbase/tailwind", + '@formbase/tailwind', ], serverExternalPackages: ['libsql', '@libsql/client'], typescript: { diff --git a/apps/web/package.json b/apps/web/package.json index 83b999f..8a2aac6 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -4,10 +4,10 @@ "private": true, "type": "module", "scripts": { - "build": "next build", - "dev": "next dev -p 3000", + "build": "dotenv -e ../../.env.local -- next build", + "dev": "dotenv -e ../../.env.local -- next dev -p 3000", "lint": "eslint . --cache --max-warnings 0", - "start": "next start", + "start": "dotenv -e ../../.env.local -- next start", "typecheck": "tsc --noEmit --tsBuildInfoFile .tsbuildinfo" }, "dependencies": { @@ -16,6 +16,7 @@ "@formbase/db": "workspace:*", "@formbase/email": "workspace:*", "@formbase/env": "workspace:*", + "@formbase/queue": "workspace:*", "@formbase/ui": "workspace:*", "@formbase/utils": "workspace:*", "@hookform/resolvers": "^3.4.2", @@ -57,6 +58,7 @@ "@types/react": "^18.3.2", "@types/react-dom": "^18.3.0", "dotenv": "^16.4.5", + "dotenv-cli": "^7.4.2", "eslint-config-formbase": "workspace:*", "postcss": "^8.4.38", "shiki": "^1.6.3", diff --git a/apps/web/src/app/(auth)/login/login.tsx b/apps/web/src/app/(auth)/login/login.tsx index 1613406..f367b5c 100644 --- a/apps/web/src/app/(auth)/login/login.tsx +++ b/apps/web/src/app/(auth)/login/login.tsx @@ -9,8 +9,6 @@ import type { FormEvent } from 'react'; import { IconBrandGithub, IconBrandGoogleFilled } from '@tabler/icons-react'; import { signIn } from '@formbase/auth/client'; - -import { Logo } from '../_components/logo'; import { Button } from '@formbase/ui/primitives/button'; import { Input } from '@formbase/ui/primitives/input'; import { Label } from '@formbase/ui/primitives/label'; @@ -20,6 +18,8 @@ import { LoadingButton } from '~/components/loading-button'; import { PasswordInput } from '~/components/password-input'; import { useSocialAuth } from '~/lib/hooks/use-social-auth'; +import { Logo } from '../_components/logo'; + export function Login() { const router = useRouter(); const [formError, setFormError] = useState(null); diff --git a/apps/web/src/app/(auth)/reset-password/[token]/page.tsx b/apps/web/src/app/(auth)/reset-password/[token]/page.tsx index 948281f..59cdf2e 100644 --- a/apps/web/src/app/(auth)/reset-password/[token]/page.tsx +++ b/apps/web/src/app/(auth)/reset-password/[token]/page.tsx @@ -1,7 +1,6 @@ import Link from 'next/link'; import { Logo } from '../../_components/logo'; - import { ResetPassword } from './reset-password'; export const metadata = { diff --git a/apps/web/src/app/(auth)/reset-password/page.tsx b/apps/web/src/app/(auth)/reset-password/page.tsx index e8ef369..8710183 100644 --- a/apps/web/src/app/(auth)/reset-password/page.tsx +++ b/apps/web/src/app/(auth)/reset-password/page.tsx @@ -4,7 +4,6 @@ import { redirect } from 'next/navigation'; import { getSession } from '@formbase/auth/server'; import { Logo } from '../_components/logo'; - import { SendResetEmail } from './send-reset-email'; export const metadata = { diff --git a/apps/web/src/app/(auth)/reset-password/send-reset-email.tsx b/apps/web/src/app/(auth)/reset-password/send-reset-email.tsx index 0a7ca5e..345e3ea 100644 --- a/apps/web/src/app/(auth)/reset-password/send-reset-email.tsx +++ b/apps/web/src/app/(auth)/reset-password/send-reset-email.tsx @@ -1,8 +1,10 @@ 'use client'; -import { type FormEvent, useState } from 'react'; +import { useState } from 'react'; import { useRouter } from 'next/navigation'; +import type { FormEvent } from 'react'; + import { toast } from 'sonner'; import { authClient } from '@formbase/auth/client'; @@ -46,10 +48,7 @@ export function SendResetEmail() { return (
-