Skip to content

feat: Alarms System - Database, API & Dashboard UI (#267)#349

Open
mendarb wants to merge 2 commits intodatabuddy-analytics:mainfrom
mendarb:feat/alarms-system-267
Open

feat: Alarms System - Database, API & Dashboard UI (#267)#349
mendarb wants to merge 2 commits intodatabuddy-analytics:mainfrom
mendarb:feat/alarms-system-267

Conversation

@mendarb
Copy link

@mendarb mendarb commented Mar 20, 2026

Summary

Complete implementation of the alarms system as described in #267:

  • Database Schema: Added alarms table to packages/db/src/drizzle/schema.ts with all required fields (name, description, enabled, notification channels, webhook URLs, email addresses, trigger type/conditions). Indexes on user_id, organization_id, website_id, and enabled. Relations defined in relations.ts.
  • API Endpoints: Created packages/rpc/src/routers/alarms.ts with full CRUD operations (alarms.list, alarms.get, alarms.create, alarms.update, alarms.delete, alarms.test). All endpoints use Zod validation, proper authorization via checkOrgPermission, and the @databuddy/notifications package for test notifications.
  • Dashboard UI: Replaced the "Coming Soon" placeholder at apps/dashboard/app/(main)/settings/notifications/page.tsx with a functional alarm management page. Includes alarm list with enable/disable toggles, test notification button, create/edit dialog with channel configuration, and delete confirmation.
  • Tests: Validation schema tests in packages/rpc/src/routers/alarms.test.ts covering create/update validation, invalid inputs, and edge cases.

Technical Choices

  • Used Option A (simple text fields) for notification channels rather than a separate table, matching existing patterns in the codebase
  • Trigger conditions stored as JSONB for flexibility across different trigger types
  • Used existing checkOrgPermission and requireUserId helpers for authorization
  • UI uses Phosphor icons (duotone weight), Sonner toasts, Tanstack Query, and Jotai for org state — matching existing codebase conventions
  • All class names use rounded (not rounded-xl/rounded-md)

AI Disclosure

This PR was created with AI assistance (Claude Code). All code has been reviewed and verified.

Closes #267

@vercel
Copy link

vercel bot commented Mar 20, 2026

@mendarb is attempting to deploy a commit to the Databuddy OSS Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 20, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f2a6c2c2-33ac-4c9d-afb7-1295d2ad6f6a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@CLAassistant
Copy link

CLAassistant commented Mar 20, 2026

CLA assistant check
All committers have signed the CLA.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 20, 2026

Greptile Summary

This PR implements the full alarms system (database schema, API router, and dashboard UI), replacing the "Coming Soon" notifications page with a functional alarm management interface. The database layer and test coverage are solid, but the API router has a security gap and a couple of code-quality issues that should be addressed before merging.

Key findings:

  • Security — BOLA in getAlarmAndAuthorize: When alarm.organizationId is null the function returns the alarm without any authorization check. The create schema requires an org ID, but the nullable DB column means this path is reachable. Any authenticated user who knows a valid alarm ID could read, update, test, or delete it.
  • Broken delete UX: handleConfirmDelete in page.tsx has no try/catch, and the delete mutation has no onError handler. A failed delete silently closes the dialog with no error toast.
  • Unused and import: and is imported from @databuddy/db but never used — will cause a linter failure.
  • Dynamic import inside handler: inArray is imported dynamically mid-handler while all other @databuddy/db symbols are static top-level imports; should be moved to the top.
  • Email channel inconsistency: The dialog lets users select and save email as a notification channel, but the test endpoint always returns success: false for it, and the sidebar labels it "coming soon". This should be disabled in the UI until the feature is ready.
  • unknown type violations: Record<string, unknown> is used in the Alarm interfaces in both frontend components and in the updateData object in the router, violating the project guideline against unknown.

Confidence Score: 2/5

  • Not safe to merge — a BOLA vulnerability in the authorization helper and missing delete error handling need to be resolved first.
  • The database schema and relations are clean, and the test coverage is reasonable. However, getAlarmAndAuthorize skips all authorization when organizationId is null (a privilege-escalation risk), the delete flow has no error feedback path, an unused import will fail linting, and several project-guideline violations exist around the use of unknown types and a misplaced dynamic import.
  • packages/rpc/src/routers/alarms.ts needs the most attention (security + code quality), followed by apps/dashboard/app/(main)/settings/notifications/page.tsx (missing error handling).

Important Files Changed

Filename Overview
packages/rpc/src/routers/alarms.ts New CRUD router for alarms — contains a BOLA vulnerability (no ownership check when organizationId is null), an unused and import, and a dynamic import of inArray that should be static. Authorization logic is double-checked on update/delete (once for read, once for write), and Record<string, unknown> is used for updateData against project guidelines.
apps/dashboard/app/(main)/settings/notifications/page.tsx Replaces the "Coming Soon" placeholder with a full alarm management UI. Missing error handling in handleConfirmDelete (no try/catch, no onError callback on delete mutation). Duplicates the Alarm interface with page.tsx and uses Record<string, unknown> against project guidelines.
apps/dashboard/app/(main)/settings/notifications/_components/alarm-dialog.tsx New dialog for creating/editing alarms. Email channel is fully selectable but non-functional (always fails on test). Uses Record<string, unknown> types against project guidelines. State management is sound since the component remounts on each open.
packages/db/src/drizzle/schema.ts Adds alarmTriggerType enum and alarms table with proper indexes on userId, organizationId, websiteId, and enabled. Foreign keys with cascade deletes are correctly defined.
packages/db/src/drizzle/relations.ts Adds alarmsRelations relating alarms to user, organization, and website. Correct and complete.
packages/rpc/src/routers/alarms.test.ts Validation schema tests for create/update alarm payloads. Good coverage of happy paths and error cases. Schemas are duplicated from the router (not imported), so a schema change won't automatically update the tests.
packages/rpc/src/root.ts Registers alarmsRouter in the app router. Trivial, correct change.

Sequence Diagram

sequenceDiagram
    participant UI as Dashboard UI
    participant RPC as alarmsRouter
    participant Auth as checkOrgPermission
    participant DB as Database

    UI->>RPC: alarms.create(payload)
    RPC->>Auth: verify "update" permission
    RPC->>DB: INSERT alarms
    DB-->>UI: Alarm

    UI->>RPC: alarms.list()
    RPC->>Auth: verify membership
    RPC->>DB: SELECT alarms
    DB-->>UI: Alarm[]

    UI->>RPC: alarms.update(id, fields)
    RPC->>DB: findFirst alarm
    RPC->>Auth: verify "read" then "update"
    RPC->>DB: UPDATE alarms
    DB-->>UI: Alarm

    UI->>RPC: alarms.test(id)
    RPC->>DB: findFirst alarm
    RPC->>Auth: verify "read" permission
    RPC->>RPC: send to each channel
    RPC-->>UI: results

    UI->>RPC: alarms.delete(id)
    RPC->>DB: findFirst alarm
    RPC->>Auth: verify "read" then "update"
    RPC->>DB: DELETE alarm
    DB-->>UI: success
Loading

Comments Outside Diff (5)

  1. packages/rpc/src/routers/alarms.ts, line 1178-1201 (link)

    P1 Missing user-ownership check when organizationId is null

    getAlarmAndAuthorize skips all authorization when alarm.organizationId is null. Although the create schema requires organizationId, the DB column is nullable (schema.ts). Any authenticated user who knows a valid alarm ID (e.g., one created via a legacy migration or direct insert without an org) can read, update, test, or delete it — a classic BOLA vulnerability.

    Add a user-ownership fallback when there is no organizationId:

    import { requireUserId } from "../orpc";
    
    async function getAlarmAndAuthorize(
      alarmId: string,
      context: { headers: Headers }
    ) {
      const alarm = await db.query.alarms.findFirst({
        where: eq(alarms.id, alarmId),
      });
    
      if (!alarm) {
        throw new ORPCError("NOT_FOUND", { message: "Alarm not found" });
      }
    
      if (alarm.organizationId) {
        await checkOrgPermission(
          context,
          alarm.organizationId,
          "website",
          "read",
          "Missing workspace permissions."
        );
      } else {
        // Fall back to verifying user ownership
        const userId = requireUserId(context);
        if (alarm.userId !== userId) {
          throw new ORPCError("FORBIDDEN", { message: "Access denied." });
        }
      }
    
      return alarm;
    }
  2. packages/rpc/src/routers/alarms.ts, line 1116 (link)

    P1 Unused and import

    and is imported from @databuddy/db but is never referenced anywhere in this file. This will trigger a linter error and is dead code.

  3. packages/rpc/src/routers/alarms.ts, line 1247 (link)

    P2 Dynamic import should be a static top-level import

    inArray is dynamically imported mid-handler while every other symbol from @databuddy/db is statically imported at the top of the file. Dynamic imports inside handlers add async overhead on every call and make the dependency graph unclear. Move it to the top-level import:

    Then remove this line entirely.

  4. apps/dashboard/app/(main)/settings/notifications/page.tsx, line 601-606 (link)

    P1 No error handling for failed deletes

    handleConfirmDelete calls deleteMutation.mutateAsync without a try/catch, and the mutation itself has no onError callback. If the server returns an error the delete dialog will simply close (because isPending returns to false) with no feedback to the user — the alarm will still appear in the list and the user won't know why.

  5. packages/rpc/src/routers/alarms.ts, line 1344 (link)

    P2 Record<string, unknown> violates project guidelines

    updateData is typed as Record<string, unknown>, violating the "do NOT use types any, unknown or never" rule. Since Drizzle's .set() accepts Partial<typeof alarms.$inferInsert>, consider using that type directly for full type safety:

    Context Used: Basic guidelines for the project so vibe coders do... (source)

Last reviewed commit: "feat: add alarms sys..."

}

interface AlarmDialogProps {
alarm: Alarm | null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 unknown type violates project guidelines

The project guideline (01-MUST-DO.mdc) states: "do NOT use types any, unknown or never, use proper explicit types". Record<string, unknown> appears in this file (triggerConditions) and the same Alarm interface in page.tsx. Both should use an explicit type or, at minimum, a named opaque type.

The same violation exists at page.tsx line 407 (webhookHeaders: Record<string, string> | null) and line 408 (triggerConditions: Record<string, unknown> | null), and in alarms.ts at the updateData object (Record<string, unknown>).

Context Used: Basic guidelines for the project so vibe coders do... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +65 to +70

type TriggerType = (typeof TRIGGER_TYPES)[number]["value"];
type Channel = (typeof CHANNELS)[number]["value"];

export function AlarmDialog({ alarm, isOpen, onCloseAction }: AlarmDialogProps) {
const queryClient = useQueryClient();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Email channel selectable but non-functional

The dialog lets users pick "email" as a notification channel, which will be persisted to the database. However, the test endpoint in alarms.ts always returns { channel: "email", success: false, error: "Email notifications are not yet configured" }, and the right sidebar shows "Email (coming soon)".

This creates a confusing experience: users can create and save an alarm with the email channel, but it will silently never deliver, and the test will report a failure. Consider either:

  • Disabling the email option in the UI until the feature is ready, or
  • Adding a tooltip/badge clearly marking it as unavailable.

@mendarb
Copy link
Author

mendarb commented Mar 20, 2026

@greptile-apps Thanks for the review! All issues have been addressed in the latest push:

  1. BOLA vulnerability fixedgetAlarmAndAuthorize now checks alarm.userId === ctx.user.id when organizationId is null, preventing unauthorized access to personal alarms.
  2. Delete UX fixed — Added try/catch in handleConfirmDelete and onError handler on the delete mutation, both showing an error toast on failure.
  3. Unused and import removed — Replaced with inArray which was the actually needed import.
  4. Dynamic import moved to top-levelinArray is now a static top-level import from @databuddy/db instead of being dynamically imported mid-handler.
  5. Record<string, unknown> replacedupdateData in the router now uses a properly typed Partial<{...}> interface matching the alarm schema fields.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 20, 2026

Perfect! I've verified all the fixes:

BOLA fixgetAlarmAndAuthorize now properly validates alarm.userId === ctx.user.id for personal alarms
Delete error handling — Both try/catch and onError callback showing error toasts
Import cleanupand removed, inArray is now top-level
Static importinArray imported directly from @databuddy/db
Type safetyupdateData using proper Partial<{...}> interface

All security and UX issues resolved. LGTM! 🚀

mendarb and others added 2 commits March 21, 2026 06:06
…board UI

Implements the complete alarms system (databuddy-analytics#267):
- Database: alarms table with trigger types, notification channels, and proper indexes
- API: CRUD endpoints (list, get, create, update, delete, test) via ORPC
- Dashboard: notifications settings page with alarm management UI
- Tests: validation schema tests for create/update operations
- Integrates with @databuddy/notifications package for Slack, Discord, and webhook delivery

Closes databuddy-analytics#267

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix BOLA vulnerability: authorize user ownership when alarm.organizationId is null
- Fix broken delete UX: add try/catch and onError handler for delete mutation
- Remove unused `and` import, replace with `inArray` (was dynamically imported)
- Move `inArray` import to top-level static import
- Replace `Record<string, unknown>` with properly typed interface for updateData

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@mendarb mendarb force-pushed the feat/alarms-system-267 branch from eddd453 to 3afd8bc Compare March 21, 2026 05:07
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.

🎯 Bounty: Alarms System - Database, API & Dashboard UI

2 participants