Skip to content

feat: Implement Alarms System backend (#267)#357

Open
K09-0 wants to merge 6 commits intodatabuddy-analytics:mainfrom
K09-0:main
Open

feat: Implement Alarms System backend (#267)#357
K09-0 wants to merge 6 commits intodatabuddy-analytics:mainfrom
K09-0:main

Conversation

@K09-0
Copy link

@K09-0 K09-0 commented Mar 23, 2026

/claim #267
/claim #268

Description

Implemented the core backend for the Alarms System as specified in the bounty.
Integrated uptime service with the new alarms logic to trigger notifications.

Changes

  • Database: Added alarms table to the Drizzle schema with fields for website_id, channels, and trigger_conditions.
  • API: Created a new alarms router with full CRUD functionality (list, get, create, update, delete).
  • Validation: Implemented strict Zod schemas for input validation and type safety.

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas

K09-0 added 3 commits March 23, 2026 16:19
Add alarms table schema
Implement alarms router with CRUD operations (list, get, create, update, delete)
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 23, 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: c9b20db8-b795-4de6-84e2-8e7ceb78320c

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.

@vercel
Copy link

vercel bot commented Mar 23, 2026

@K09-0 is attempting to deploy a commit to the Databuddy OSS Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 23, 2026

Greptile Summary

This PR introduces the backend for an Alarms System — a new alarms Drizzle schema table and a corresponding oRPC router with CRUD operations. Unfortunately, neither file is in a deployable state: the router imports from a module that does not exist and is not wired into the app, and the schema definition uses an invalid Drizzle API.

Key issues found:

  • Wrong framework API: alarms.ts imports protectedProcedure and router from "../trpc", a file that does not exist. The project migrated from tRPC to oRPC (@orpc/server). The router must be rewritten using oRPC's .route().input().output().handler() pattern as seen in every other router (e.g. annotations.ts).
  • Router not registered: alarmsRouter is never added to appRouter in root.ts, so all endpoints are unreachable even if the build error were fixed.
  • No authorization: Every procedure is missing a withWorkspace() call — any authenticated user can list, create, update, or delete alarms for any website they don't own. This is a significant security gap.
  • Invalid Drizzle method: The schema uses .notNullable() throughout; the correct Drizzle API is .notNull(). This will throw a TypeError at startup.
  • Missing schema constraints: No foreign key from website_idwebsites.id (with onDelete: cascade), no btree index on website_id, and no exported Alarm / AlarmInsert types — all of which are standard practice for every other table in this schema.

Confidence Score: 0/5

  • This PR is not safe to merge — it will not compile and introduces an authorization vulnerability.
  • The router imports from a non-existent module (../trpc) causing a guaranteed build failure, is not registered in root.ts so all endpoints are unreachable, uses an invalid Drizzle method (.notNullable()) that will crash at startup, and has zero authorization checks allowing any authenticated user to access any website's alarms. All four issues are blocking-critical.
  • Both changed files require substantial rework: packages/rpc/src/routers/alarms.ts must be rewritten using the oRPC API with workspace authorization, and packages/db/src/drizzle/schema.ts needs .notNull(), a foreign key with cascade, a website_id index, and type exports.

Important Files Changed

Filename Overview
packages/rpc/src/routers/alarms.ts New alarms CRUD router that will not compile — imports from a non-existent ../trpc module (project uses oRPC), uses the tRPC procedure API (.query/.mutation with ctx), is not registered in root.ts, and has no workspace-level authorization checks on any endpoint.
packages/db/src/drizzle/schema.ts New alarms table added to the schema using .notNullable() (invalid Drizzle API — should be .notNull()), missing a foreignKey constraint and cascade to websites, missing a btree index on website_id, and missing exported Alarm/AlarmInsert types.

Sequence Diagram

sequenceDiagram
    participant Client
    participant appRouter
    participant alarmsRouter
    participant withWorkspace
    participant DB

    Note over appRouter,alarmsRouter: ❌ alarmsRouter not registered in root.ts

    Client->>appRouter: alarms.list({ websiteId })
    appRouter-->>Client: ❌ Endpoint not found

    Note over alarmsRouter: If router were registered & fixed:
    Client->>alarmsRouter: alarms.list({ websiteId })
    alarmsRouter->>withWorkspace: verify user has read access to websiteId
    Note over withWorkspace: ❌ Currently missing — no auth check
    withWorkspace-->>alarmsRouter: workspace context
    alarmsRouter->>DB: SELECT * FROM alarms WHERE website_id = ?
    DB-->>alarmsRouter: alarm rows
    alarmsRouter-->>Client: Alarm[]

    Client->>alarmsRouter: alarms.create({ websiteId, channels, triggerConditions })
    alarmsRouter->>withWorkspace: verify user has update access
    Note over withWorkspace: ❌ Currently missing
    withWorkspace-->>alarmsRouter: workspace context
    alarmsRouter->>DB: INSERT INTO alarms VALUES (...)
    DB-->>alarmsRouter: new alarm
    alarmsRouter-->>Client: Alarm
Loading

Reviews (1): Last reviewed commit: "Refactor alarms router and validation sc..." | Re-trigger Greptile

Comment on lines +3 to +4
import { protectedProcedure, router } from "../trpc";
import { alarms } from "@databuddy/db/schema";
Copy link
Contributor

Choose a reason for hiding this comment

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

P0 Import from non-existent tRPC module

The entire router imports protectedProcedure and router from "../trpc", but no trpc.ts file exists in this package. The project has migrated to oRPC (@orpc/server). The correct module is "../orpc", and it does not export a router helper — routers are plain objects in this codebase. This will cause a build-time module-not-found error.

The pattern used by every other router (e.g. annotations.ts) is:

import { protectedProcedure, publicProcedure } from "../orpc";

And procedures are defined via .route({...}).input(...).output(...).handler(async ({ context, input }) => { ... }) instead of .query()/.mutation() with { ctx, input }.

The entire router needs to be rewritten to use the oRPC API.

Comment on lines +22 to +85
list: protectedProcedure
.input(z.object({ websiteId: z.string() }))
.query(async ({ ctx, input }) => {
return await ctx.db
.select()
.from(alarms)
.where(eq(alarms.websiteId, input.websiteId));
}),

get: protectedProcedure
.input(z.object({ id: z.string() }))
.query(async ({ ctx, input }) => {
const result = await ctx.db
.select()
.from(alarms)
.where(eq(alarms.id, input.id))
.limit(1);

if (!result.length) throw new Error("Alarm not found");
return result[0];
}),

create: protectedProcedure
.input(createAlarmSchema)
.mutation(async ({ ctx, input }) => {
const newAlarm = await ctx.db
.insert(alarms)
.values({
id: createId(),
websiteId: input.websiteId,
channels: input.channels,
triggerConditions: input.triggerConditions,
})
.returning();

return newAlarm[0];
}),

update: protectedProcedure
.input(updateAlarmSchema)
.mutation(async ({ ctx, input }) => {
const updatedAlarm = await ctx.db
.update(alarms)
.set({
...input.data,
updatedAt: new Date(),
})
.where(eq(alarms.id, input.id))
.returning();

if (!updatedAlarm.length) throw new Error("Alarm not found");
return updatedAlarm[0];
}),

delete: protectedProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => {
const deletedAlarm = await ctx.db
.delete(alarms)
.where(eq(alarms.id, input.id))
.returning();

if (!deletedAlarm.length) throw new Error("Alarm not found");
return deletedAlarm[0];
Copy link
Contributor

Choose a reason for hiding this comment

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

P0 Missing workspace authorization — any authenticated user can access any website's alarms

None of the procedures call withWorkspace() to verify that the authenticated user actually has access to the target website. This means any user with a valid session can list, create, update, or delete alarms for any website in the system, including websites they don't own.

Every other router that operates on website-scoped data (annotations, goals, funnels, etc.) gates access through withWorkspace, for example:

await withWorkspace(context, {
    websiteId: input.websiteId,
    permissions: ["read"], // or ["update"] for mutations
});

Additionally, get, update, and delete accept an alarm id without first loading the associated websiteId and checking access against it — an attacker can enumerate and manipulate alarms across all websites.

This needs to be addressed before the router goes live.

Comment on lines +1119 to +1127
// --- ALARMS SYSTEM ---
export const alarms = pgTable("alarms", {
id: text("id").primaryKey(), // ID аларма (обычно генерируется через createId)
websiteId: text("website_id").notNullable(), // Привязка к сайту
channels: jsonb("channels").$type<string[]>().notNullable().default([]), // ['slack', 'discord', 'email']
triggerConditions: jsonb("trigger_conditions").notNullable(), // Условия срабатывания
createdAt: timestamp("created_at").defaultNow().notNullable(),
updatedAt: timestamp("updated_at").defaultNow().notNullable(),
});
Copy link
Contributor

Choose a reason for hiding this comment

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

P0 .notNullable() is not a valid Drizzle ORM method — should be .notNull()

Every existing column in this schema that is non-nullable uses .notNull(). Drizzle ORM's column builder API exposes .notNull(), not .notNullable(). Calling .notNullable() will throw a TypeError: ... is not a function at startup.

Suggested change
// --- ALARMS SYSTEM ---
export const alarms = pgTable("alarms", {
id: text("id").primaryKey(), // ID аларма (обычно генерируется через createId)
websiteId: text("website_id").notNullable(), // Привязка к сайту
channels: jsonb("channels").$type<string[]>().notNullable().default([]), // ['slack', 'discord', 'email']
triggerConditions: jsonb("trigger_conditions").notNullable(), // Условия срабатывания
createdAt: timestamp("created_at").defaultNow().notNullable(),
updatedAt: timestamp("updated_at").defaultNow().notNullable(),
});
export const alarms = pgTable("alarms", {
id: text("id").primaryKey().notNull(),
websiteId: text("website_id").notNull(),
channels: jsonb("channels").$type<string[]>().notNull().default([]),
triggerConditions: jsonb("trigger_conditions").$type<{ status: string; durationMinutes: number }>().notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});

Comment on lines +1119 to +1127
// --- ALARMS SYSTEM ---
export const alarms = pgTable("alarms", {
id: text("id").primaryKey(), // ID аларма (обычно генерируется через createId)
websiteId: text("website_id").notNullable(), // Привязка к сайту
channels: jsonb("channels").$type<string[]>().notNullable().default([]), // ['slack', 'discord', 'email']
triggerConditions: jsonb("trigger_conditions").notNullable(), // Условия срабатывания
createdAt: timestamp("created_at").defaultNow().notNullable(),
updatedAt: timestamp("updated_at").defaultNow().notNullable(),
});
Copy link
Contributor

Choose a reason for hiding this comment

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

P1 Missing website_id index, foreign key constraint, and type exports

Every other table with a website_id foreign key in this schema has:

  1. A btree index on website_id for query performance (e.g. index("alarms_website_id_idx").using("btree", table.websiteId.asc().nullsLast().op("text_ops"))).
  2. A foreignKey constraint referencing websites.id with .onDelete("cascade") so alarms are cleaned up when a website is deleted.
  3. Corresponding type exports at the bottom of the file (export type Alarm = typeof alarms.$inferSelect; export type AlarmInsert = typeof alarms.$inferInsert;).

Without the foreign key + cascade, orphaned alarm rows will accumulate. Without the index, listing alarms per website will degrade to a full table scan.

Comment on lines +1 to +5
import { z } from "zod";
import { eq } from "drizzle-orm";
import { protectedProcedure, router } from "../trpc";
import { alarms } from "@databuddy/db/schema";
import { createId } from "@paralleldrive/cuid2";
Copy link
Contributor

Choose a reason for hiding this comment

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

P1 alarmsRouter is not registered in root.ts

packages/rpc/src/root.ts defines appRouter by composing all sub-routers, but alarmsRouter is never imported or added to that object. Even after the other issues are fixed, these endpoints will be completely unreachable through the API.

alarmsRouter needs to be imported and added:

// root.ts
import { alarmsRouter } from "./routers/alarms";
// ...
export const appRouter = {
  alarms: alarmsRouter,
  // existing entries...
};

@K09-0
Copy link
Author

K09-0 commented Mar 23, 2026

"I've implemented the basic schema and router. Since I'm working in a web editor, I couldn't run the migrations locally. Please let me know if you need me to adjust the types or add anything else to the implementation."

Implement notification trigger logic
@CLAassistant
Copy link

CLAassistant commented Mar 23, 2026

CLA assistant check
All committers have signed the CLA.

@K09-0
Copy link
Author

K09-0 commented Mar 23, 2026

Fixed the import path for notifications. The logic is now correctly linked to the uptime service. Ready for review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants