# Backend Safety Rails

> Three UUID rules prevent silent zero-row deletes and 204 responses that lie. Resource path params that can be human slugs (MUL-123) or UUIDs must go through loadIssueForUser / loadAgentForUser first; the resolved .ID is then used for all subsequent queries. Pure-UUID inputs from the wire use parseUUIDOrBadRequest which returns 400 on failure. Trusted round-trips (sqlc results, test fixtures) may use MustParseUUID which panics — a deliberate signal that unvalidated user input reached it. WS listeners (activity, autopilot, runtime sweeper) exist only to invalidate queries; they never mutate client state. Every handler that performs a write must ask “where did this UUID come from?”

- Repository: multica-ai/multica
- GitHub: https://github.com/multica-ai/multica
- Human wiki: https://grok-wiki.com/public/wiki/multica-ai-multica-fd1dd916d3bf
- Complete Markdown: https://grok-wiki.com/public/wiki/multica-ai-multica-fd1dd916d3bf/llms-full.txt

## Source Files

- `server/internal/handler/handler.go`
- `server/internal/util/pgx.go`
- `server/cmd/server/main.go`
- `server/cmd/server/listeners.go`
- `server/cmd/server/runtime_sweeper.go`
- `server/internal/handler/agent.go`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:
- [server/internal/handler/handler.go](server/internal/handler/handler.go)
- [server/internal/util/pgx.go](server/internal/util/pgx.go)
- [server/internal/util/pgx_test.go](server/internal/util/pgx_test.go)
- [CLAUDE.md](CLAUDE.md)
- [server/cmd/server/main.go](server/cmd/server/main.go)
- [server/cmd/server/listeners.go](server/cmd/server/listeners.go)
- [server/cmd/server/runtime_sweeper.go](server/cmd/server/runtime_sweeper.go)
- [server/internal/handler/agent.go](server/internal/handler/agent.go)
- [server/internal/handler/issue.go](server/internal/handler/issue.go)
- [server/internal/handler/daemon.go](server/internal/handler/daemon.go)
- [packages/core/realtime/use-realtime-sync.ts](packages/core/realtime/use-realtime-sync.ts)
</details>

# Backend Safety Rails

Three UUID handling rules in the Go backend prevent silent zero-row `DELETE` and `UPDATE` operations that return 204 success while performing no work. The rules distinguish three sources of identifiers and force every write path to obtain a validated, canonical `pgtype.UUID` before calling any `Queries.Delete*` or `Queries.Update*`. Resource path parameters that accept both human-readable slugs (e.g., `MUL-123`) and UUIDs must travel through a dedicated loader (`loadIssueForUser`, `loadAgentForUser`, `loadSkillForUser`, or `requireDaemonRuntimeAccess`). Pure-UUID values arriving on the wire use `parseUUIDOrBadRequest`. Trusted round-trips from sqlc or test fixtures use the panicking `parseUUID` (MustParseUUID) variant. WebSocket listeners for activity, autopilot, and the runtime sweeper exist only to publish invalidation signals; they never mutate client-side server state.

The contract is summarized in the project guidelines and enforced at the handler layer so that a malformed or slug identifier can never silently become a zero-valued UUID that matches zero rows.

## The Historical Failure

Before the current `ParseUUID` implementation, the utility silently returned a valid zero-valued `pgtype.UUID` on any input it could not parse. A `DELETE` using that zero UUID would execute without error, the `CommandTag` would report zero rows affected, yet the handler would still write 204. Issue #1661 was the concrete case that made the pattern intolerable.

```go
// server/internal/util/pgx.go:11-28
// ParseUUID parses s into a pgtype.UUID. Invalid input returns an error
// instead of a zero-valued UUID — silently dropping bad input has caused
// data-loss bugs (e.g. DELETE matching no rows, returning 204 success).
func ParseUUID(s string) (pgtype.UUID, error) { ... }
```

The test suite now explicitly guards the invariant:

```go
// server/internal/util/pgx_test.go:25-28
// Returning a valid zero-UUID was the root cause of #1661.
if u.Valid {
    t.Fatalf("expected u.Valid = false for %q, got true", s)
}
```

Sources: [server/internal/util/pgx.go:11-28](), [server/internal/util/pgx_test.go:25-28]()

## The Three Rules

Every handler that touches the database for a write must answer one question: "Where did this UUID come from?"

| Identifier Source | Required Gate | Helper | Failure Behavior | Typical Call Sites |
|-------------------|---------------|--------|------------------|--------------------|
| Resource path that may be human slug (`MUL-123`) or UUID (issues, agents, skills, daemon runtimes) | Dedicated loader that resolves to an entity first | `loadIssueForUser`, `loadAgentForUser`, `loadSkillForUser`, `requireDaemonRuntimeAccess` | 404 (or 400 for workspace) | `DeleteIssue`, `UpdateAgent`, `DeleteSkill`, task enqueue paths |
| Pure-UUID arriving on the wire (request body, header, URL param that is always a UUID) | Explicit validation at the boundary | `parseUUIDOrBadRequest(w, s, fieldName)` | 400 Bad Request, `ok=false` | `runtime_id`, `task_id`, inbox item IDs, most body fields |
| Trusted round-trip (sqlc result passed back inside the same request, test fixtures) | Panicking helper (signals bug if raw user input reaches it) | `parseUUID(s)` / `util.MustParseUUID` | panic (recovered to 500) | `existing.ID` after a loader, IDs from `Queries.Get*` inside the handler |

The authoritative statement of the rules lives in the developer conventions:

```markdown
# CLAUDE.md:171-180
- Resource path params that accept either a UUID or a human-readable identifier MUST be resolved through the dedicated loader...
- Pure-UUID inputs from request boundaries MUST be validated with `parseUUIDOrBadRequest`...
- Trusted UUID round-trips use `parseUUID(s)` which calls `util.MustParseUUID` and panics on invalid input...
When adding a `Queries.Delete*` or `Queries.Update*` call, ask: "Where did this UUID come from?"
```

Sources: [CLAUDE.md:171-180](), [server/internal/handler/handler.go:167-196]()

## Loaders in Practice

`loadIssueForUser` demonstrates the dual-format path required for human-readable identifiers:

```go
// server/internal/handler/handler.go:466-505
func (h *Handler) loadIssueForUser(...) (db.Issue, bool) {
    ...
    if issue, ok := h.resolveIssueByIdentifier(...); ok { return issue, true }
    issueUUID, err := util.ParseUUID(issueID)  // only after slug attempt fails
    ...
    issue, err := h.Queries.GetIssueInWorkspace(..., issueUUID, ...)
    return issue, true
}
```

After the loader succeeds, every subsequent write uses the resolved `issue.ID`:

```go
// server/internal/handler/issue.go:2382-2402
func (h *Handler) DeleteIssue(...) {
    id := chi.URLParam(r, "id")
    issue, ok := h.loadIssueForUser(w, r, id)
    if !ok { return }
    h.TaskService.CancelTasksForIssue(..., issue.ID)
    h.Queries.FailAutopilotRunsByIssue(..., issue.ID)
    ...
    err := h.Queries.DeleteIssue(..., issue.ID)  // never the raw string
}
```

`loadAgentForUser` and `requireDaemonRuntimeAccess` follow the same shape, each calling `parseUUIDOrBadRequest` internally so that a bad UUID never reaches a query.

Sources: [server/internal/handler/handler.go:466-505](), [server/internal/handler/handler.go:574-602](), [server/internal/handler/issue.go:2382-2402](), [server/internal/handler/daemon.go:72-100]()

## Trusted Round-Trip Path

Inside a handler, once an entity has been obtained from the database or a loader, its ID may be passed to another query using the thin wrapper:

```go
// server/internal/handler/handler.go:167-179
// parseUUID is intentionally the panicking variant...
// A panic here means an unguarded user-input string slipped in.
func parseUUID(s string) pgtype.UUID { return util.MustParseUUID(s) }
```

The panic is deliberate: it turns a latent bug into an immediate 500 during development and CI rather than a silent no-op in production.

## Realtime Listeners and Invalidation Contract

Activity, autopilot, and runtime-sweeper listeners publish events (e.g., `daemon:register` with `action: "stale_sweep"`) so that connected clients know to refresh data. The sweeper itself mutates the database; the published event is only a notification.

```go
// server/cmd/server/runtime_sweeper.go:150-165
for wsID := range workspaces {
    bus.Publish(events.Event{
        Type: protocol.EventDaemonRegister,
        ...
        Payload: map[string]any{"action": "stale_sweep"},
    })
}
```

On the client, `useRealtimeSync` reacts to these events exclusively by calling `queryClient.invalidateQueries`:

```ts
// packages/core/realtime/use-realtime-sync.ts:271
qc.invalidateQueries({ queryKey: runtimeKeys.all(wsId) })
```

No server-derived data is ever written directly into a Zustand store from a WebSocket handler. Zustand owns only client state (current workspace, filters, drafts, modal visibility). React Query owns all server state; WS events keep it fresh via invalidation.

Registration of the listeners occurs once in `main.go` after the event bus and broadcaster are created.

Sources: [server/cmd/server/runtime_sweeper.go:150-165](), [packages/core/realtime/use-realtime-sync.ts:159-182](), [server/cmd/server/main.go:245-320]()

## Summary

The safety rails are a narrow, enforceable contract at the HTTP handler boundary. By forcing every write path to declare the provenance of its UUIDs, the system eliminates an entire class of "delete succeeded but actually did nothing" bugs. Loaders, `parseUUIDOrBadRequest`, and the panicking trusted helper each correspond to one source of truth for an identifier. WebSocket listeners remain pure invalidation signals, preserving the single-source-of-truth guarantee between React Query and the database. The pattern is documented in CLAUDE.md and verified by the test that would have caught #1661.

Sources: [CLAUDE.md:171-180]()
