Skip to content

A tenant-keyed singleton has no REST shape: OpList answers a one-element envelope and OpRead asks the client for its own tenant id #166

Description

@jryannel

The shape

A table keyed by the tenant column — one row per caller — has no REST shape a resource can generate. From the studio-apps port (SQLB-PORT-FEEDBACK.md finding 35), billing's billing_subscriptions is keyed by org_id, which is also the scope column. The two ops on offer are both wrong:

  • OpList answers a one-element {items:[…]} envelope for a resource that is definitionally singular — every client unwraps items[0] forever;
  • OpRead at /billing-subscriptions/{org_id} asks the client to send its own tenant id, a value the server already holds and the BeforeQuery hook already enforces — so the path parameter is either redundant or a lie, and a mismatch is a 404 that means "you typed your own name wrong".

The port's conclusion was to expose nothing and keep the one handler hand-written — the right call today, and the reason this is worth a spelling: the module kept a handler only because the generated surface cannot say "the caller's row".

Why it recurs

The port names the class: "every payments, webhook or sync-target table in this platform has it" — any table whose writers are a third party (PSP webhooks, a reconcile worker) and whose one client-facing operation is "show me mine". Settings-per-tenant, profile-per-user, subscription-per-org are all this shape. Each one is currently a permanent hand-written handler beside an otherwise fully-declared module.

What a singleton op could be

OpSingleton (or rest.Singleton): GET <path> — no {id} segment — answering the caller's one row as a bare object, confined by the same BeforeQuery hook every other read runs, 404 when the hook's predicate matches nothing. The Scoped obligation applies unchanged; in fact the op only makes sense on a Scoped table, which is checkable at mount. Optionally OpUpdate on the same path for the settings-table variant where the client may write its own row.

This is the same family as #101 (read-only mount as a first-class shape): the mounts an adopting application actually reaches for, offered as declarations rather than left as the residue of a port.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions