Summary
The unique indexes on workflow_triggers are global, not team-scoped, so in a multi-tenant deployment two teams cannot register the same webhook path or the same workflow-name + cron schedule — the second team's mantle apply fails with a unique-constraint violation.
From migration 003_triggers.sql:
CREATE UNIQUE INDEX idx_workflow_triggers_webhook_path ON workflow_triggers (path) WHERE type = 'webhook' AND enabled = true;
CREATE UNIQUE INDEX idx_workflow_triggers_cron ON workflow_triggers (workflow_name, schedule) WHERE type = 'cron';
Neither includes team_id, even though workflow_definitions is team-scoped (UNIQUE(team_id, name, version)) and the trigger read paths carry team_id.
Context
Surfaced during review of #162 (Codex P2). Trigger registration was dead before #162, so these inserts never ran and the latent collision was unreachable; #162 makes it reachable. Single-tenant deployments (the default team) are unaffected, which is why it wasn't blocking for #162.
The wrinkle for webhooks
The cron case is a straightforward fix (add team_id to the unique index). Webhooks are not: the inbound POST /hooks/<path> handler resolves team via auth.TeamIDFromContext(r.Context()), which is always the default team for unauthenticated webhooks (see internal/server/webhook.go + LookupWebhookTrigger). So webhook routing isn't team-aware today — making the path team-unique at the DB level would let two teams register the same path, but the handler couldn't disambiguate which team's workflow to run. A correct fix needs a routing decision (e.g. team-prefixed webhook paths, or a per-team webhook token) alongside the index change.
Proposed work
- Cron/email: migration to make the uniqueness team-scoped (e.g.
(team_id, workflow_name, schedule) for cron).
- Webhook: decide the multi-tenant routing model (team-prefixed path or per-team token), then scope the path index accordingly.
- Update
internal/workflow trigger reconciliation and the server read/handler paths to match.
- Tests covering two teams registering the same path / same workflow-name+schedule.
Files
internal/db/migrations/003_triggers.sql (+ a new migration to alter the indexes)
internal/workflow/triggers.go, internal/server/webhook.go, internal/server/trigger.go
Summary
The unique indexes on
workflow_triggersare global, not team-scoped, so in a multi-tenant deployment two teams cannot register the same webhook path or the same workflow-name + cron schedule — the second team'smantle applyfails with a unique-constraint violation.From migration
003_triggers.sql:Neither includes
team_id, even thoughworkflow_definitionsis team-scoped (UNIQUE(team_id, name, version)) and the trigger read paths carryteam_id.Context
Surfaced during review of #162 (Codex P2). Trigger registration was dead before #162, so these inserts never ran and the latent collision was unreachable; #162 makes it reachable. Single-tenant deployments (the default team) are unaffected, which is why it wasn't blocking for #162.
The wrinkle for webhooks
The cron case is a straightforward fix (add
team_idto the unique index). Webhooks are not: the inboundPOST /hooks/<path>handler resolves team viaauth.TeamIDFromContext(r.Context()), which is always the default team for unauthenticated webhooks (seeinternal/server/webhook.go+LookupWebhookTrigger). So webhook routing isn't team-aware today — making the path team-unique at the DB level would let two teams register the same path, but the handler couldn't disambiguate which team's workflow to run. A correct fix needs a routing decision (e.g. team-prefixed webhook paths, or a per-team webhook token) alongside the index change.Proposed work
(team_id, workflow_name, schedule)for cron).internal/workflowtrigger reconciliation and the server read/handler paths to match.Files
internal/db/migrations/003_triggers.sql(+ a new migration to alter the indexes)internal/workflow/triggers.go,internal/server/webhook.go,internal/server/trigger.go