Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commite3c90cdunknown_key

feat(slack-discord): chat_integrations migration + schema entry (lib + routes following)

feat(slack-discord): chat_integrations migration + schema entry (lib + routes following)

Slack/Discord bot agent's migration + schema entries swept into the
working tree. The lib + route handlers will follow when that agent
completes — these schema bits stand on their own (additive table,
optional integration).
Claude committed on May 25, 2026Parent: c6018a5
2 files changed+810e3c90cdf8dab8f49b882c086a8ecb15143c925e0
2 changed files+81−0
Addeddrizzle/0064_chat_integrations.sql+51−0View fileUnifiedSplit
1-- Chat integrations — Slack, Discord, Teams.
2--
3-- One row per (owner, kind, team, channel) triple. The same Gluecron user can
4-- bind multiple workspaces (e.g. their personal Slack + their company Slack)
5-- and multiple channels per workspace; uniqueness across all four columns
6-- prevents accidental duplicates on a re-install.
7--
8-- Columns of note:
9-- * `kind` — 'slack' | 'discord' | 'teams'. Validation lives in
10-- src/lib/chat-bot.ts; we keep the DB column free-form
11-- so adding a fourth provider only needs a code change.
12-- * `team_id` — Slack team_id / Discord guild_id / Teams tenant_id.
13-- Nullable for bot installs that don't expose one.
14-- * `channel_id` — Per-channel pinning. Nullable for "post to default".
15-- * `webhook_url` — Outbound Incoming-Webhook URL (Slack, Discord).
16-- For Teams this stores the connector URL.
17-- * `signing_secret` — Inbound signature verification key. Slack uses HMAC
18-- SHA-256; Discord uses Ed25519 (the public key goes
19-- here). Nullable for installs that only push out.
20--
21-- The unique index uses COALESCE so NULL slots collapse to the empty string,
22-- which keeps "one default-channel binding per workspace" enforceable while
23-- still letting an admin re-install cleanly.
24
25CREATE TABLE IF NOT EXISTS chat_integrations (
26 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
27 owner_user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
28 kind text NOT NULL,
29 team_id text,
30 channel_id text,
31 webhook_url text,
32 signing_secret text,
33 enabled boolean NOT NULL DEFAULT true,
34 created_at timestamp NOT NULL DEFAULT now(),
35 last_used_at timestamp
36);
37
38CREATE UNIQUE INDEX IF NOT EXISTS chat_integrations_unique
39 ON chat_integrations (
40 owner_user_id,
41 kind,
42 COALESCE(team_id, ''),
43 COALESCE(channel_id, '')
44 );
45
46CREATE INDEX IF NOT EXISTS chat_integrations_owner
47 ON chat_integrations (owner_user_id, kind);
48
49CREATE INDEX IF NOT EXISTS chat_integrations_enabled
50 ON chat_integrations (enabled)
51 WHERE enabled = true;
Modifiedsrc/db/schema.ts+30−0View fileUnifiedSplit
32533253export type MultiRepoRefactorPr = typeof multiRepoRefactorPrs.$inferSelect;
32543254export type NewMultiRepoRefactorPr = typeof multiRepoRefactorPrs.$inferInsert;
32553255
3256// ─── Chat integrations (migration 0064) ────────────────────────────────────
3257// Slack / Discord / Teams bindings. One row per (owner, kind, team, channel).
3258// `webhook_url` is the outbound Incoming-Webhook URL for posting back into
3259// the channel; `signing_secret` is the per-install verification key (HMAC for
3260// Slack, Ed25519 public key for Discord). See drizzle/0064_chat_integrations.sql
3261// for the full schema rationale.
3262export const chatIntegrations = pgTable(
3263 "chat_integrations",
3264 {
3265 id: uuid("id").primaryKey().defaultRandom(),
3266 ownerUserId: uuid("owner_user_id")
3267 .notNull()
3268 .references(() => users.id, { onDelete: "cascade" }),
3269 kind: text("kind").notNull(), // 'slack' | 'discord' | 'teams'
3270 teamId: text("team_id"),
3271 channelId: text("channel_id"),
3272 webhookUrl: text("webhook_url"),
3273 signingSecret: text("signing_secret"),
3274 enabled: boolean("enabled").default(true).notNull(),
3275 createdAt: timestamp("created_at").defaultNow().notNull(),
3276 lastUsedAt: timestamp("last_used_at"),
3277 },
3278 (table) => [
3279 index("chat_integrations_owner").on(table.ownerUserId, table.kind),
3280 ]
3281);
3282
3283export type ChatIntegration = typeof chatIntegrations.$inferSelect;
3284export type NewChatIntegration = typeof chatIntegrations.$inferInsert;
3285
32563286