AdminService

Base path /squiddy.v1.AdminService/ · defined in proto/squiddy/v1/admin.proto

AdminService is the authenticated control-plane RPC surface. In EPIC-2 it exposes WhoAmI (the minimal endpoint proving admin OIDC login works end to end) and the member invitation flow; EPIC-3 grows it into the full control-plane.

Authentication

Every procedure requires an authenticated principal — the session the dashboard login establishes, or an OIDC ID token presented as a bearer credential. Most procedures additionally carry the tenant_id they act on and are authorised against the caller's role in that tenant; the role each one needs is stated with the procedure. A resource belonging to another tenant is reported as not_found, never distinguished from a missing one.

Procedures

ProcedureSummary
WhoAmIWhoAmI returns the identity of the authenticated principal making the call.
ListMyMembershipsListMyMemberships returns every tenant membership held by the authenticated caller — the tenant, its display name and the caller's role in it — so the dashboard shell can populate its tenant switcher (US 6.1.2).
InviteMemberInviteMember provisions a member end to end (US 17.2.3): it pre-creates the invitee's account on the identity provider when needed, grants them the chosen role on the target tenant, and emails them branded access instructions in the requested language.
CreateOrganizationCreateOrganization lets an authenticated caller who belongs to no tenant bootstrap their own organization and become its Owner (US 14.2.2) — the self-serve onboarding path.
GetEmailVerificationGetEmailVerification reports whether the authenticated caller has proved they own the email address on their account (US 9.1.5).
ResendEmailVerificationResendEmailVerification asks the identity provider to send the authenticated caller a fresh verification message (US 9.1.5).
GetOrganizationAllowanceGetOrganizationAllowance reports how many organizations the authenticated caller already owns and how many one identity may own (US 9.1.6), so the onboarding screen can state where the caller stands BEFORE they fill the form instead of turning the cap into a surprise at submit.
GetOnboardingGetOnboarding reports how far an organization has got through the first-run quickstart, and whether somebody has put the wizard away (US 9.2.1).
UpdateOnboardingUpdateOnboarding records how far the organization has got and whether the wizard is put away (US 9.2.1).
ListMembersListMembers returns every member of a tenant — the identity behind each membership and the role held — so an Admin can review the roster before changing a role or removing someone.
UpdateMemberRoleUpdateMemberRole changes an existing member's role within a tenant and returns the updated member.
RemoveMemberRemoveMember removes a member from a tenant.
GenerateApiKeyGenerateApiKey mints a new data-plane API key scoped to a (tenant, environment) and returns the plaintext key exactly once (US 2.2.1): the server stores only its hash, so the secret in the response can never be recovered afterwards.
RenameApiKeyRenameApiKey changes the display name of an existing key the caller manages (US 2.2.1).
RevokeApiKeyRevokeApiKey revokes a key so it can no longer authenticate a request (US 2.2.1).
ListApiKeysListApiKeys returns every API key of a tenant as non-secret metadata — id, environment, name, display prefix, created/last-used timestamps and revocation state — so an Editor can review and manage keys from the dashboard (US 6.7.2).
CreateProjectCreateProject creates a project (a group of apps) under a tenant and returns it.
GetProjectGetProject returns a single project by id, scoped to the caller's tenant.
ListProjectsListProjects returns every project of the tenant, ordered by id.
UpdateProjectUpdateProject changes a project's display name (the slug is its stable handle and is immutable).
DeleteProjectDeleteProject deletes an empty project.
CreateEnvironmentCreateEnvironment creates a deployment stage (e.g. dev/qa/prod) under a project and returns it.
GetEnvironmentGetEnvironment returns a single environment by id, scoped to the caller's tenant.
ListEnvironmentsListEnvironments returns every environment of a project, ordered by id.
UpdateEnvironmentUpdateEnvironment changes an environment's name, unique within its project.
DeleteEnvironmentDeleteEnvironment deletes an empty environment.
CreateConfigurationCreateConfiguration creates the draft configuration for (environment, app_id) and returns it.
GetConfigurationGetConfiguration returns the current (non-archived) configuration for (environment, app_id).
ListConfigurationsListConfigurations returns every current (non-archived) configuration in an environment, ordered by app_id.
UpdateConfigurationUpdateConfiguration records the new body as the current working configuration for (environment, app_id), validating it first.
DeleteConfigurationDeleteConfiguration archives (soft-deletes) the current configuration for (environment, app_id), preserving history.
ListConfigurationVersionsListConfigurationVersions returns the full version history of (environment, app_id), newest version first, so an Editor can review every version and pick a rollback target.
GetConfigurationVersionGetConfigurationVersion returns one specific version of (environment, app_id) with its full body, so the dashboard can diff two versions body-to-body (US 6.8.1) — ListConfigurationVersions is metadata-only, and this fetches a single historical body on demand.
RollbackConfigurationRollbackConfiguration re-materializes a prior version's body as the current working draft: a published current version is never mutated (a new draft is forked), so version history stays immutable.
PublishConfigurationPublishConfiguration promotes a specific version of (environment, app_id) from draft to published, atomically demoting whatever version was published before, so at most one version is ever published per (environment, app_id).
ListAuditLogListAuditLog returns the tenant's audit trail — every configuration mutation (create/update/delete/publish/rollback), newest first — recording who acted, when, and the before/after diff.
PreviewResolvePreviewResolve simulates what a client with the given context would receive for a configuration, before it is published.
ValidateConfigurationValidateConfiguration checks a candidate config body against the exact same domain validation that gates a publish (US 3.4.1) — structural schema, the typed semantic rules, and the unknown-key check — and returns the issues without writing anything.

WhoAmI

POST /squiddy.v1.AdminService/WhoAmI

WhoAmI returns the identity of the authenticated principal making the call. It requires authentication: the auth interceptor rejects an anonymous request with CodeUnauthenticated before the handler runs, so a successful response always describes a validated principal.

Errors: internal, invalid_argument, permission_denied, unauthenticated

WhoAmIRequest

Request of WhoAmI.

WhoAmIRequest carries no fields: the caller is identified by the authenticated principal on the request context, never by request data (which would be spoofable).

_No fields._

WhoAmIResponse

Response of WhoAmI.

WhoAmIResponse echoes the authenticated principal's identity. subject is the stable OIDC sub claim (the durable account key); email and name are the display claims, which may be empty when the provider does not supply them. is_platform_admin says whether the caller's account carries the platform administrator flag, so the dashboard can offer the back-office entry point only to whoever can use it (US 18.2.4): it is a rendering hint, never the authorization itself — every PlatformService procedure re-reads the flag server-side, so a client that forges it gains no authority.

#FieldJSONTypePresenceConstraints
1subjectsubjectstringsingular—
2emailemailstringsingular—
3namenamestringsingular—
4is_platform_adminisPlatformAdminboolsingular—

ListMyMemberships

POST /squiddy.v1.AdminService/ListMyMemberships

ListMyMemberships returns every tenant membership held by the authenticated caller — the tenant, its display name and the caller's role in it — so the dashboard shell can populate its tenant switcher (US 6.1.2). Like WhoAmI it needs only a validated principal, never a pre-existing tenant membership: every member reads their own memberships, and a caller with none receives an empty list rather than an error.

Errors: internal, invalid_argument, permission_denied, unauthenticated

ListMyMembershipsRequest

Request of ListMyMemberships.

ListMyMembershipsRequest carries no fields: the caller is identified by the authenticated principal on the request context, never by request data (which would be spoofable).

_No fields._

ListMyMembershipsResponse

Response of ListMyMemberships.

ListMyMembershipsResponse returns the caller's memberships, ordered by tenant id. It is empty when the authenticated principal belongs to no tenant yet (e.g. before being invited into one), which the shell renders as an empty tenant switcher rather than an error.

#FieldJSONTypePresenceConstraints
1membershipsmembershipsTenantMembershiprepeated—

InviteMember

POST /squiddy.v1.AdminService/InviteMember

InviteMember provisions a member end to end (US 17.2.3): it pre-creates the invitee's account on the identity provider when needed, grants them the chosen role on the target tenant, and emails them branded access instructions in the requested language. It requires the caller to manage members on the target tenant (an Admin or Owner); minting an Owner is reserved to Owners. The response carries the provisioned Member and whether the email was sent — email delivery is best-effort, so a member is still provisioned when it fails.

Errors: already_exists, internal, invalid_argument, permission_denied, unauthenticated

InviteMemberRequest

Request of InviteMember.

InviteMemberRequest describes the member to provision. tenant_id is the tenant the invitee will join; the caller must be authorized to manage members on it. email is the invitee's address — the account is provisioned on the identity provider for it, and it receives the invitation email. role is the membership role to grant, one of owner/admin/editor/viewer; inviting an owner is reserved to owners. name is the invitee's optional display name (max 200 runes). language selects the email language ("" defaults to English; "it" or "en"). resend, when true, re-sends the invitation to an existing member without changing their role, instead of failing with AlreadyExists.

email carries the same 254-rune ceiling ProvisionTenant's owner_email does — RFC 5321's maximum for a complete address. The two procedures reach the identity provider through one shared path, so an address one of them refuses on sight and the other carries all the way to a 500 would be the same bug fixed on one channel only (BUG #825).

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0
2emailemailstringsingularat most 254 characters; a valid email address
3rolerolestringsingularone of owner, admin, editor, viewer
4namenamestringsingularat most 200 characters
5languagelanguagestringsingularone of "", it, en
6resendresendboolsingular—

InviteMemberResponse

Response of InviteMember.

InviteMemberResponse returns the provisioned member (identity, role and join time, with a "never accessed" last-seen until the invitee first signs in) and whether the invitation email was actually sent. email_sent is false when delivery failed: the member is still provisioned, and the caller can offer to re-send.

#FieldJSONTypePresenceConstraints
1membermemberMembersingular—
2email_sentemailSentboolsingular—

CreateOrganization

POST /squiddy.v1.AdminService/CreateOrganization

CreateOrganization lets an authenticated caller who belongs to no tenant bootstrap their own organization and become its Owner (US 14.2.2) — the self-serve onboarding path. It requires only a validated principal, never a pre-existing membership: the caller has none yet, so it is the one write gated on authentication alone rather than a per-tenant role. In a single transaction it creates the tenant, a default project and environment, and an Owner membership for the caller, and records an audit entry (actor = the caller). The name is validated; a slug derived from it that another tenant already holds is reported as AlreadyExists.

Errors: already_exists, internal, invalid_argument, permission_denied, unauthenticated

CreateOrganizationRequest

Request of CreateOrganization.

CreateOrganizationRequest carries the display name of the organization to create (US 14.2.2). The caller is identified by the authenticated principal on the request context — its OIDC subject becomes the Owner — never by request data. The tenant's stable slug is derived from the name server-side, so it is not part of the request.

#FieldJSONTypePresenceConstraints
1namenamestringsingularat least 1 characters; at most 200 characters

CreateOrganizationResponse

Response of CreateOrganization.

CreateOrganizationResponse returns the membership the caller now holds on the organization they just created — the tenant's id, stable slug and display name, and the caller's role (always owner). It is the same TenantMembership shape ListMyMemberships returns, so the dashboard can set the new tenant as active and enter without a second round-trip.

#FieldJSONTypePresenceConstraints
1membershipmembershipTenantMembershipsingular—

GetEmailVerification

POST /squiddy.v1.AdminService/GetEmailVerification

GetEmailVerification reports whether the authenticated caller has proved they own the email address on their account (US 9.1.5). CreateOrganization refuses a caller who has not, so the onboarding screen asks this first and offers the explanation instead of a form it knows the server will reject. Like WhoAmI it needs only a validated principal.

The answer is deliberately NOT read from the session alone: a token minted before the person verified would keep saying "unverified" for the whole life of the session, so a caller who verified a minute ago would still be locked out until they signed in again. When the session's claim does not already assert verification, the identity provider is asked directly.

Errors: internal, invalid_argument, permission_denied, unauthenticated

GetEmailVerificationRequest

Request of GetEmailVerification.

GetEmailVerificationRequest carries no fields: the account inspected is always the authenticated principal's own, resolved from the request context. Accepting a subject or an address here would turn a self-check into a directory lookup.

_No fields._

GetEmailVerificationResponse

Response of GetEmailVerification.

GetEmailVerificationResponse reports the caller's email address and whether it has been verified (US 9.1.5). verified=false is also the answer when verification could not be established at all — the check fails closed, exactly as CreateOrganization's refusal does, so the screen and the enforcement can never disagree. email is the address the message would be sent to, shown so the caller can tell "no mail arrived" from "it went to an address I mistyped"; it is empty when the provider supplied none.

#FieldJSONTypePresenceConstraints
1emailemailstringsingular—
2verifiedverifiedboolsingular—

ResendEmailVerification

POST /squiddy.v1.AdminService/ResendEmailVerification

ResendEmailVerification asks the identity provider to send the authenticated caller a fresh verification message (US 9.1.5). It exists because the hosted registration screen leaves the address unverified without sending anything, so without it the only way out of the "verify your email" state would be a message that was never sent. Like WhoAmI it needs only a validated principal, and it only ever targets the caller's own account — never an address from the request, which would make it a way to mail arbitrary people.

Errors: internal, invalid_argument, permission_denied, unauthenticated

ResendEmailVerificationRequest

Request of ResendEmailVerification.

ResendEmailVerificationRequest carries no fields: the message always goes to the authenticated caller's own address, taken from the identity provider, never from the request.

_No fields._

ResendEmailVerificationResponse

Response of ResendEmailVerification.

ResendEmailVerificationResponse reports what became of the request. sent is true when the identity provider accepted the send. already_verified is true when there was nothing to send because the address is verified already — not a failure, and the screen answers it by showing the form the caller was waiting for rather than an error.

#FieldJSONTypePresenceConstraints
1sentsentboolsingular—
2already_verifiedalreadyVerifiedboolsingular—

GetOrganizationAllowance

POST /squiddy.v1.AdminService/GetOrganizationAllowance

GetOrganizationAllowance reports how many organizations the authenticated caller already owns and how many one identity may own (US 9.1.6), so the onboarding screen can state where the caller stands BEFORE they fill the form instead of turning the cap into a surprise at submit. Like WhoAmI it needs only a validated principal, and it counts nobody else's organizations.

The limit travels on the wire because it is a server rule: a copy of the number in the SPA would be a second place to change it, and the two would eventually disagree about the very fact the screen is there to state.

Errors: internal, invalid_argument, permission_denied, unauthenticated

GetOrganizationAllowanceRequest

Request of GetOrganizationAllowance.

GetOrganizationAllowanceRequest carries no fields: the count is always the authenticated caller's own, taken from the request context, never from the request.

_No fields._

GetOrganizationAllowanceResponse

Response of GetOrganizationAllowance.

GetOrganizationAllowanceResponse states where the caller stands against the per-identity cap on organizations (US 9.1.6). owned counts the organizations the caller OWNS — one they were invited into belongs to somebody else and does not count — and a suspended organization is counted like any other, because suspending one does not give its owner room for another. limit is the maximum one identity may own. at_limit is the verdict CreateOrganization itself applies, sent rather than re-derived so the screen and the enforcement can never disagree about where the boundary is.

#FieldJSONTypePresenceConstraints
1ownedownedint32singular—
2limitlimitint32singular—
3at_limitatLimitboolsingular—

GetOnboarding

POST /squiddy.v1.AdminService/GetOnboarding

GetOnboarding reports how far an organization has got through the first-run quickstart, and whether somebody has put the wizard away (US 9.2.1). The state belongs to the ORGANIZATION, not to the browser that created it: it is asked of the server precisely so that signing out, opening another browser or picking up another device resumes at the step reached instead of starting over — which is what a localStorage flag could never do.

total_steps travels on the wire for the same reason GetOrganizationAllowance sends its limit: how long the ladder is, and therefore what counts as finished, is a server rule, and a second copy of it in the SPA would eventually disagree with the enforcement.

Errors: internal, invalid_argument, permission_denied, unauthenticated

GetOnboardingRequest

Request of GetOnboarding.

GetOnboardingRequest names the organization whose quickstart state to read. The caller must be authorized to view it — the state is a property of the tenant, so it is scoped like any other tenant-scoped read rather than to the caller's own account.

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0

GetOnboardingResponse

Response of GetOnboarding.

GetOnboardingResponse carries the organization's quickstart state.

#FieldJSONTypePresenceConstraints
1statestateOnboardingStatesingular—

UpdateOnboarding

POST /squiddy.v1.AdminService/UpdateOnboarding

UpdateOnboarding records how far the organization has got and whether the wizard is put away (US 9.2.1). The step is monotonic server-side — a value below the one already reached is ignored, never applied — so a stale client, a second tab or a slow response can no more rewind somebody's progress than a lost packet can.

Dismissal is a separate fact from the step, and deliberately reversible: closing the wizard must not throw away the progress, because the dashboard offers to reopen it (which is this same procedure with dismissed=false). Once the ladder is finished the wizard is gone for good — that verdict is the step's business, not the dismissal's.

Errors: internal, invalid_argument, permission_denied, unauthenticated

UpdateOnboardingRequest

Request of UpdateOnboarding.

UpdateOnboardingRequest names the organization and the state to record. step is the rung the caller believes it has reached; the server keeps the higher of that and the stored one, so sending a stale value is harmless rather than destructive. dismissed sets whether the wizard is put away, in either direction — closing it and reopening it from the dashboard are the same call with a different flag.

The wire rule only refuses a rung below the first; the upper bound is checked by the handler, where the ladder's length is defined, so the contract does not have to be edited to add a step.

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0
2stepstepint32singularat least 1
3dismisseddismissedboolsingular—

UpdateOnboardingResponse

Response of UpdateOnboarding.

UpdateOnboardingResponse returns the state as stored after the update — the step actually kept (which may be higher than the one sent) and the dismissal — so the caller renders what the server holds rather than what it hoped to write.

#FieldJSONTypePresenceConstraints
1statestateOnboardingStatesingular—

ListMembers

POST /squiddy.v1.AdminService/ListMembers

ListMembers returns every member of a tenant — the identity behind each membership and the role held — so an Admin can review the roster before changing a role or removing someone. Requires manage-members on the tenant; the handler scopes the read to it.

Errors: internal, invalid_argument, permission_denied, unauthenticated

ListMembersRequest

Request of ListMembers.

ListMembersRequest names the tenant whose members to list. The caller must be authorized to manage members on it.

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0

ListMembersResponse

Response of ListMembers.

ListMembersResponse returns the tenant's members, ordered by user id.

#FieldJSONTypePresenceConstraints
1membersmembersMemberrepeated—

UpdateMemberRole

POST /squiddy.v1.AdminService/UpdateMemberRole

UpdateMemberRole changes an existing member's role within a tenant and returns the updated member. Requires manage-members; touching an Owner (setting a member to Owner, or changing an Owner's role) is reserved to Owners. Demoting the last Owner is refused with FailedPrecondition so a tenant is never left ownerless. A user with no membership on the tenant is reported as not found.

Errors: failed_precondition, internal, invalid_argument, not_found, permission_denied, unauthenticated

UpdateMemberRoleRequest

Request of UpdateMemberRole.

UpdateMemberRoleRequest identifies the member by (tenant_id, user_id) and carries the new role to grant, one of owner/admin/editor/viewer. Setting or changing an Owner is reserved to Owners, and the last Owner cannot be demoted.

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0
2user_iduserIdint64singulargreater than 0
3rolerolestringsingularone of owner, admin, editor, viewer

UpdateMemberRoleResponse

Response of UpdateMemberRole.

UpdateMemberRoleResponse returns the member with its new role.

#FieldJSONTypePresenceConstraints
1membermemberMembersingular—

RemoveMember

POST /squiddy.v1.AdminService/RemoveMember

RemoveMember removes a member from a tenant. Requires manage-members; removing an Owner is reserved to Owners, and removing the last Owner is refused with FailedPrecondition. A user with no membership on the tenant is reported as not found. Removal is idempotent only in effect — a second call for an already-removed member is a not-found.

Errors: failed_precondition, internal, invalid_argument, not_found, permission_denied, unauthenticated

RemoveMemberRequest

Request of RemoveMember.

RemoveMemberRequest identifies the member to remove by (tenant_id, user_id). Removing an Owner is reserved to Owners, and the last Owner cannot be removed.

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0
2user_iduserIdint64singulargreater than 0

RemoveMemberResponse

Response of RemoveMember.

RemoveMemberResponse echoes the user id of the removed member.

#FieldJSONTypePresenceConstraints
1user_iduserIdint64singular—

GenerateApiKey

POST /squiddy.v1.AdminService/GenerateApiKey

GenerateApiKey mints a new data-plane API key scoped to a (tenant, environment) and returns the plaintext key exactly once (US 2.2.1): the server stores only its hash, so the secret in the response can never be recovered afterwards. It requires the caller to edit configs on the target tenant (Editor or above); the handler scopes that check to the tenant the key belongs to.

Errors: internal, invalid_argument, permission_denied, unauthenticated

GenerateApiKeyRequest

Request of GenerateApiKey.

GenerateApiKeyRequest describes the key to mint. tenant_id and environment_id are the scope the key authenticates as; the caller must be authorized to edit configs on tenant_id. name is the human label shown alongside the key. (The acceptance scope is tenant+environment; the app/project a request targets stays in the data-plane request within that scope, US 2.2.2.)

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0
2environment_idenvironmentIdint64singulargreater than 0
3namenamestringsingularat least 1 characters; at most 100 characters

GenerateApiKeyResponse

Response of GenerateApiKey.

GenerateApiKeyResponse carries the freshly minted key. api_key is the plaintext secret, returned exactly once — it is not recoverable after this response, since only its hash is stored. id, prefix and name are the non-secret metadata the client keeps to manage the key later (the prefix lets a user recognise it in a list without the secret).

#FieldJSONTypePresenceConstraints
1api_keyapiKeystringsingular—
2ididint64singular—
3prefixprefixstringsingular—
4namenamestringsingular—

RenameApiKey

POST /squiddy.v1.AdminService/RenameApiKey

RenameApiKey changes the display name of an existing key the caller manages (US 2.2.1). It requires the edit-config permission on the key's tenant; a key id outside that tenant is reported as not found.

Errors: internal, invalid_argument, not_found, permission_denied, unauthenticated

RenameApiKeyRequest

Request of RenameApiKey.

RenameApiKeyRequest identifies the key to rename. tenant_id is the tenant that owns the key and that the caller must be authorized to edit; id is the key; name is the new label.

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0
2ididint64singulargreater than 0
3namenamestringsingularat least 1 characters; at most 100 characters

RenameApiKeyResponse

Response of RenameApiKey.

RenameApiKeyResponse echoes the renamed key's stable metadata (no secret is involved in a rename).

#FieldJSONTypePresenceConstraints
1ididint64singular—
2namenamestringsingular—
3prefixprefixstringsingular—

RevokeApiKey

POST /squiddy.v1.AdminService/RevokeApiKey

RevokeApiKey revokes a key so it can no longer authenticate a request (US 2.2.1). Revocation is immediate and idempotent. It requires the edit-config permission on the key's tenant; a key id outside that tenant is reported as not found.

Errors: internal, invalid_argument, not_found, permission_denied, unauthenticated

RevokeApiKeyRequest

Request of RevokeApiKey.

RevokeApiKeyRequest identifies the key to revoke. tenant_id is the owning tenant the caller must be authorized to edit; id is the key.

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0
2ididint64singulargreater than 0

RevokeApiKeyResponse

Response of RevokeApiKey.

RevokeApiKeyResponse echoes the revoked key's id and the revocation time as an RFC 3339 timestamp string, after which the key no longer authenticates a request.

#FieldJSONTypePresenceConstraints
1ididint64singular—
2revoked_atrevokedAtstringsingular—

ListApiKeys

POST /squiddy.v1.AdminService/ListApiKeys

ListApiKeys returns every API key of a tenant as non-secret metadata — id, environment, name, display prefix, created/last-used timestamps and revocation state — so an Editor can review and manage keys from the dashboard (US 6.7.2). The plaintext secret is never returned (it exists only in GenerateApiKey's one-time response); revoked keys are included so they stay auditable. It requires the edit-config permission on the tenant, like the other API-key procedures, and the handler scopes the read to that tenant.

Errors: internal, invalid_argument, permission_denied, unauthenticated

ListApiKeysRequest

Request of ListApiKeys.

ListApiKeysRequest names the tenant whose API keys to list. The caller must be authorized to edit configs on it.

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0

ListApiKeysResponse

Response of ListApiKeys.

ListApiKeysResponse returns the tenant's API keys as metadata, ordered by id. Both active and revoked keys are included so a revoked key stays visible for audit.

#FieldJSONTypePresenceConstraints
1api_keysapiKeysApiKeyrepeated—

CreateProject

POST /squiddy.v1.AdminService/CreateProject

CreateProject creates a project (a group of apps) under a tenant and returns it. The slug is optional: left empty it is derived from the display name, exactly as CreateOrganization derives a tenant slug, so a UI caller need only collect a name. Either way it must be unique within the tenant. Requires config:edit on the tenant.

Errors: internal, invalid_argument, permission_denied, unauthenticated

CreateProjectRequest

Request of CreateProject.

CreateProjectRequest names the tenant to create the project under, its display name, and optionally its slug (unique within the tenant, lowercase kebab). An empty slug is derived from the name server-side, so the field carries the rules only when populated — hence the IGNORE_IF_ZERO_VALUE, without which the pattern would reject the empty string the SPA sends. A caller that needs a specific stable handle (an SDK integration matching an existing identifier) still sets it explicitly.

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0
2slugslugstringsingularat least 1 characters; at most 63 characters; matches ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$; ignore = 1
3namenamestringsingularat least 1 characters; at most 200 characters

CreateProjectResponse

Response of CreateProject.

CreateProjectResponse returns the created project.

#FieldJSONTypePresenceConstraints
1projectprojectProjectsingular—

GetProject

POST /squiddy.v1.AdminService/GetProject

GetProject returns a single project by id, scoped to the caller's tenant. A project in another tenant is reported as not found. Requires config:view on the tenant.

Errors: internal, invalid_argument, not_found, permission_denied, unauthenticated

GetProjectRequest

Request of GetProject.

GetProjectRequest identifies the project by id within the caller's tenant.

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0
2ididint64singulargreater than 0

GetProjectResponse

Response of GetProject.

GetProjectResponse returns the requested project.

#FieldJSONTypePresenceConstraints
1projectprojectProjectsingular—

ListProjects

POST /squiddy.v1.AdminService/ListProjects

ListProjects returns every project of the tenant, ordered by id. Requires config:view.

Errors: internal, invalid_argument, permission_denied, unauthenticated

ListProjectsRequest

Request of ListProjects.

ListProjectsRequest names the tenant whose projects to list.

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0

ListProjectsResponse

Response of ListProjects.

ListProjectsResponse returns the tenant's projects, ordered by id.

#FieldJSONTypePresenceConstraints
1projectsprojectsProjectrepeated—

UpdateProject

POST /squiddy.v1.AdminService/UpdateProject

UpdateProject changes a project's display name (the slug is its stable handle and is immutable). A project in another tenant is not found. Requires config:edit.

Errors: internal, invalid_argument, not_found, permission_denied, unauthenticated

UpdateProjectRequest

Request of UpdateProject.

UpdateProjectRequest identifies the project and carries its new display name (the slug is immutable).

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0
2ididint64singulargreater than 0
3namenamestringsingularat least 1 characters; at most 200 characters

UpdateProjectResponse

Response of UpdateProject.

UpdateProjectResponse returns the updated project.

#FieldJSONTypePresenceConstraints
1projectprojectProjectsingular—

DeleteProject

POST /squiddy.v1.AdminService/DeleteProject

DeleteProject deletes an empty project. It fails with FailedPrecondition when the project still has environments (no silent cascade). Requires config:edit.

Errors: failed_precondition, internal, invalid_argument, permission_denied, unauthenticated

DeleteProjectRequest

Request of DeleteProject.

DeleteProjectRequest identifies the project to delete within the caller's tenant.

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0
2ididint64singulargreater than 0

DeleteProjectResponse

Response of DeleteProject.

DeleteProjectResponse echoes the id of the deleted project.

#FieldJSONTypePresenceConstraints
1ididint64singular—

CreateEnvironment

POST /squiddy.v1.AdminService/CreateEnvironment

CreateEnvironment creates a deployment stage (e.g. dev/qa/prod) under a project and returns it. The name must be unique within the project. Requires config:edit.

Errors: internal, invalid_argument, permission_denied, unauthenticated

CreateEnvironmentRequest

Request of CreateEnvironment.

CreateEnvironmentRequest names the tenant and the project to create the environment under, and its name (unique within the project, e.g. dev/qa/prod).

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0
2project_idprojectIdint64singulargreater than 0
3namenamestringsingularat least 1 characters; at most 63 characters

CreateEnvironmentResponse

Response of CreateEnvironment.

CreateEnvironmentResponse returns the created environment.

#FieldJSONTypePresenceConstraints
1environmentenvironmentEnvironmentsingular—

GetEnvironment

POST /squiddy.v1.AdminService/GetEnvironment

GetEnvironment returns a single environment by id, scoped to the caller's tenant. An environment in another tenant is reported as not found. Requires config:view.

Errors: internal, invalid_argument, not_found, permission_denied, unauthenticated

GetEnvironmentRequest

Request of GetEnvironment.

GetEnvironmentRequest identifies the environment by id within the caller's tenant.

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0
2ididint64singulargreater than 0

GetEnvironmentResponse

Response of GetEnvironment.

GetEnvironmentResponse returns the requested environment.

#FieldJSONTypePresenceConstraints
1environmentenvironmentEnvironmentsingular—

ListEnvironments

POST /squiddy.v1.AdminService/ListEnvironments

ListEnvironments returns every environment of a project, ordered by id. Requires config:view.

Errors: internal, invalid_argument, permission_denied, unauthenticated

ListEnvironmentsRequest

Request of ListEnvironments.

ListEnvironmentsRequest names the tenant and the project whose environments to list.

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0
2project_idprojectIdint64singulargreater than 0

ListEnvironmentsResponse

Response of ListEnvironments.

ListEnvironmentsResponse returns the project's environments, ordered by id.

#FieldJSONTypePresenceConstraints
1environmentsenvironmentsEnvironmentrepeated—

UpdateEnvironment

POST /squiddy.v1.AdminService/UpdateEnvironment

UpdateEnvironment changes an environment's name, unique within its project. An environment in another tenant is not found. Requires config:edit.

Errors: internal, invalid_argument, not_found, permission_denied, unauthenticated

UpdateEnvironmentRequest

Request of UpdateEnvironment.

UpdateEnvironmentRequest identifies the environment and carries its new name.

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0
2ididint64singulargreater than 0
3namenamestringsingularat least 1 characters; at most 63 characters

UpdateEnvironmentResponse

Response of UpdateEnvironment.

UpdateEnvironmentResponse returns the updated environment.

#FieldJSONTypePresenceConstraints
1environmentenvironmentEnvironmentsingular—

DeleteEnvironment

POST /squiddy.v1.AdminService/DeleteEnvironment

DeleteEnvironment deletes an empty environment. It fails with FailedPrecondition when the environment still has configurations (no silent cascade). Requires config:edit.

Errors: failed_precondition, internal, invalid_argument, permission_denied, unauthenticated

DeleteEnvironmentRequest

Request of DeleteEnvironment.

DeleteEnvironmentRequest identifies the environment to delete within the caller's tenant.

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0
2ididint64singulargreater than 0

DeleteEnvironmentResponse

Response of DeleteEnvironment.

DeleteEnvironmentResponse echoes the id of the deleted environment.

#FieldJSONTypePresenceConstraints
1ididint64singular—

CreateConfiguration

POST /squiddy.v1.AdminService/CreateConfiguration

CreateConfiguration creates the draft configuration for (environment, app_id) and returns it. The body is validated against the configuration schema; a malformed body is rejected with InvalidArgument. A configuration for the pair already existing is AlreadyExists. Requires config:edit. Publishing the draft is a separate step (F3.2).

Errors: already_exists, internal, invalid_argument, permission_denied, unauthenticated

CreateConfigurationRequest

Request of CreateConfiguration.

CreateConfigurationRequest names the tenant and environment, the app_id the configuration is for, and the JSON body. The body is required and validated against the configuration schema in the handler.

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0
2environment_idenvironmentIdint64singulargreater than 0
3app_idappIdstringsingularat least 1 characters; at most 200 characters
4config_bodyconfigBodygoogle.protobuf.Structsingularrequired

CreateConfigurationResponse

Response of CreateConfiguration.

CreateConfigurationResponse returns the created draft configuration.

#FieldJSONTypePresenceConstraints
1configurationconfigurationConfigurationsingular—

GetConfiguration

POST /squiddy.v1.AdminService/GetConfiguration

GetConfiguration returns the current (non-archived) configuration for (environment, app_id). Requires config:view.

Errors: internal, invalid_argument, permission_denied, unauthenticated

GetConfigurationRequest

Request of GetConfiguration.

GetConfigurationRequest identifies the configuration by (environment, app_id) within the caller's tenant.

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0
2environment_idenvironmentIdint64singulargreater than 0
3app_idappIdstringsingularat least 1 characters; at most 200 characters

GetConfigurationResponse

Response of GetConfiguration.

GetConfigurationResponse returns the requested configuration.

#FieldJSONTypePresenceConstraints
1configurationconfigurationConfigurationsingular—

ListConfigurations

POST /squiddy.v1.AdminService/ListConfigurations

ListConfigurations returns every current (non-archived) configuration in an environment, ordered by app_id. Requires config:view.

Errors: internal, invalid_argument, permission_denied, unauthenticated

ListConfigurationsRequest

Request of ListConfigurations.

ListConfigurationsRequest names the tenant and environment whose configurations to list, and the keyset page window (US #599 / M5). page_size is the maximum number of configurations to return (0 = server default, capped server-side); page_token is the opaque next_page_token from a prior response ("" starts at the first page).

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0
2environment_idenvironmentIdint64singulargreater than 0
3page_sizepageSizeint32singularat least 0
4page_tokenpageTokenstringsingularat most 512 characters

ListConfigurationsResponse

Response of ListConfigurations.

ListConfigurationsResponse returns one metadata-only summary per current configuration, ordered by app_id (US #599 / M5). next_page_token is the cursor to pass as the next request's page_token; it is empty on the last page. Bodies are fetched on demand with GetConfiguration.

#FieldJSONTypePresenceConstraints
1configurationsconfigurationsConfigurationSummaryrepeated—
2next_page_tokennextPageTokenstringsingular—

UpdateConfiguration

POST /squiddy.v1.AdminService/UpdateConfiguration

UpdateConfiguration records the new body as the current working configuration for (environment, app_id), validating it first. Editing an unpublished draft mutates it in place; editing a published (immutable) version forks a new draft version instead (US 3.2.1). A missing configuration is not found. Requires config:edit.

Errors: internal, invalid_argument, not_found, permission_denied, unauthenticated

UpdateConfigurationRequest

Request of UpdateConfiguration.

UpdateConfigurationRequest identifies the configuration by (environment, app_id) and carries the replacement JSON body, required and validated as on create.

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0
2environment_idenvironmentIdint64singulargreater than 0
3app_idappIdstringsingularat least 1 characters; at most 200 characters
4config_bodyconfigBodygoogle.protobuf.Structsingularrequired

UpdateConfigurationResponse

Response of UpdateConfiguration.

UpdateConfigurationResponse returns the updated configuration.

#FieldJSONTypePresenceConstraints
1configurationconfigurationConfigurationsingular—

DeleteConfiguration

POST /squiddy.v1.AdminService/DeleteConfiguration

DeleteConfiguration archives (soft-deletes) the current configuration for (environment, app_id), preserving history. A missing configuration is not found. Requires config:edit.

Errors: internal, invalid_argument, not_found, permission_denied, unauthenticated

DeleteConfigurationRequest

Request of DeleteConfiguration.

DeleteConfigurationRequest identifies the configuration by (environment, app_id) to archive within the caller's tenant.

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0
2environment_idenvironmentIdint64singulargreater than 0
3app_idappIdstringsingularat least 1 characters; at most 200 characters

DeleteConfigurationResponse

Response of DeleteConfiguration.

DeleteConfigurationResponse echoes the id of the archived configuration.

#FieldJSONTypePresenceConstraints
1ididint64singular—

ListConfigurationVersions

POST /squiddy.v1.AdminService/ListConfigurationVersions

ListConfigurationVersions returns the full version history of (environment, app_id), newest version first, so an Editor can review every version and pick a rollback target. Both live and archived versions are included. Requires config:view.

Errors: internal, invalid_argument, permission_denied, unauthenticated

ListConfigurationVersionsRequest

Request of ListConfigurationVersions.

ListConfigurationVersionsRequest identifies the configuration by (environment, app_id) whose version history to list, within the caller's tenant, and the keyset page window (US #599 / M5). page_size is the maximum number of versions to return (0 = server default, capped server-side); page_token is the opaque next_page_token from a prior response ("" starts at the newest version).

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0
2environment_idenvironmentIdint64singulargreater than 0
3app_idappIdstringsingularat least 1 characters; at most 200 characters
4page_sizepageSizeint32singularat least 0
5page_tokenpageTokenstringsingularat most 512 characters

ListConfigurationVersionsResponse

Response of ListConfigurationVersions.

ListConfigurationVersionsResponse returns one metadata-only summary per version, newest first (US #599 / M5). next_page_token is the cursor to pass as the next request's page_token; it is empty on the last page. A specific version's body is fetched on demand with GetConfigurationVersion.

#FieldJSONTypePresenceConstraints
1versionsversionsConfigurationSummaryrepeated—
2next_page_tokennextPageTokenstringsingular—

GetConfigurationVersion

POST /squiddy.v1.AdminService/GetConfigurationVersion

GetConfigurationVersion returns one specific version of (environment, app_id) with its full body, so the dashboard can diff two versions body-to-body (US 6.8.1) — ListConfigurationVersions is metadata-only, and this fetches a single historical body on demand. A missing or archived version is not found. Requires config:view.

Errors: internal, invalid_argument, not_found, permission_denied, unauthenticated

GetConfigurationVersionRequest

Request of GetConfigurationVersion.

GetConfigurationVersionRequest identifies the configuration by (environment, app_id) and the specific version whose full body to fetch, within the caller's tenant (US 6.8.1).

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0
2environment_idenvironmentIdint64singulargreater than 0
3app_idappIdstringsingularat least 1 characters; at most 200 characters
4versionversionint32singulargreater than 0

GetConfigurationVersionResponse

Response of GetConfigurationVersion.

GetConfigurationVersionResponse returns the requested version, body included.

#FieldJSONTypePresenceConstraints
1configurationconfigurationConfigurationsingular—

RollbackConfiguration

POST /squiddy.v1.AdminService/RollbackConfiguration

RollbackConfiguration re-materializes a prior version's body as the current working draft: a published current version is never mutated (a new draft is forked), so version history stays immutable. A missing target version is not found. Requires config:edit.

Errors: internal, invalid_argument, not_found, permission_denied, unauthenticated

RollbackConfigurationRequest

Request of RollbackConfiguration.

RollbackConfigurationRequest identifies the configuration by (environment, app_id) and the prior version whose body to re-materialize as the current working draft.

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0
2environment_idenvironmentIdint64singulargreater than 0
3app_idappIdstringsingularat least 1 characters; at most 200 characters
4versionversionint32singulargreater than 0

RollbackConfigurationResponse

Response of RollbackConfiguration.

RollbackConfigurationResponse returns the resulting working configuration (a mutated draft or a freshly forked draft).

#FieldJSONTypePresenceConstraints
1configurationconfigurationConfigurationsingular—

PublishConfiguration

POST /squiddy.v1.AdminService/PublishConfiguration

PublishConfiguration promotes a specific version of (environment, app_id) from draft to published, atomically demoting whatever version was published before, so at most one version is ever published per (environment, app_id). It stamps the publish lineage (who published it and when). Until a version is published it stays invisible to the data-plane, which serves only the published version. A missing or archived target version is not found. Requires config:edit.

Errors: internal, invalid_argument, not_found, permission_denied, unauthenticated

PublishConfigurationRequest

Request of PublishConfiguration.

PublishConfigurationRequest identifies the configuration by (environment, app_id) and the version to promote from draft to published, within the caller's tenant.

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0
2environment_idenvironmentIdint64singulargreater than 0
3app_idappIdstringsingularat least 1 characters; at most 200 characters
4versionversionint32singulargreater than 0

PublishConfigurationResponse

Response of PublishConfiguration.

PublishConfigurationResponse returns the now-published configuration, carrying the publish lineage (published_by / published_at) the transition stamped.

#FieldJSONTypePresenceConstraints
1configurationconfigurationConfigurationsingular—

ListAuditLog

POST /squiddy.v1.AdminService/ListAuditLog

ListAuditLog returns the tenant's audit trail — every configuration mutation (create/update/delete/publish/rollback), newest first — recording who acted, when, and the before/after diff. The trail is immutable (append-only) and read is an Admin capability, so this requires the manage-members permission (Admin or Owner); a Viewer or Editor is denied.

Errors: internal, invalid_argument, permission_denied, unauthenticated

ListAuditLogRequest

Request of ListAuditLog.

ListAuditLogRequest names the tenant whose audit trail to list, and the keyset page window (US #599 / M5). The trail is append-only and unbounded, so it is always paginated: page_size is the maximum number of entries to return (0 = server default, capped server-side); page_token is the opaque next_page_token from a prior response ("" starts at the newest entry).

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0
2page_sizepageSizeint32singularat least 0
3page_tokenpageTokenstringsingularat most 512 characters

ListAuditLogResponse

Response of ListAuditLog.

ListAuditLogResponse returns the tenant's audit entries, newest first (US #599 / M5). next_page_token is the cursor to pass as the next request's page_token; it is empty on the last page.

#FieldJSONTypePresenceConstraints
1entriesentriesAuditLogEntryrepeated—
2next_page_tokennextPageTokenstringsingular—

PreviewResolve

POST /squiddy.v1.AdminService/PreviewResolve

PreviewResolve simulates what a client with the given context would receive for a configuration, before it is published. It runs the client context through the exact same resolution engine as the data-plane (no divergent copy) over the configuration's own body — by default the current working version (the latest draft when one exists), or a specific version when version is set — so an Editor can verify rules and targeting first. It is read-only and requires config:view. A missing configuration or version is not found.

Errors: internal, invalid_argument, not_found, permission_denied, unauthenticated

PreviewResolveRequest

Request of PreviewResolve.

PreviewResolveRequest identifies the configuration to preview by (environment, app_id) within the caller's tenant, and carries the client context to simulate. version selects which version's body to resolve: 0 (unset) previews the current working version — the latest draft when one exists — while a positive value previews that specific version. The context fields mirror a data-plane request's targeting inputs (app_version/app_type/app_mode/language/custom_params plus entity_id); the data-plane's app_bundle_id and lang_fingerprint are intentionally absent, as neither affects which configuration blocks match, and the preview always returns the full resolved payload.

entity_id simulates the rollout bucketing key of US 4.5.1, so the editor can ask "what would THIS user receive" and get the same answer the data plane would give: the preview runs the identical engine, and the bucket is a pure hash of the entity and the rollout's salt. Leaving it empty previews a client that sends no entity id, which the engine buckets by app_id.

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0
2environment_idenvironmentIdint64singulargreater than 0
3app_idappIdstringsingularat least 1 characters; at most 200 characters
4versionversionint32singularat least 0
5app_versionappVersionstringsingularat least 1 characters; at most 50 characters
6app_typeappTypestringsingularat least 1 characters; at most 50 characters
7app_modeappModestringsingularat least 1 characters; at most 50 characters
8languagelanguagestringsingularat least 1 characters; at most 50 characters
9custom_paramscustomParamsstringrepeated—
10entity_identityIdstringsingularat most 200 characters

PreviewResolveResponse

Response of PreviewResolve.

PreviewResolveResponse carries the resolved configuration exactly as the data-plane would return it to a client for the simulated context (reusing GetRemoteConfigurationResponse), so the preview is verifiably identical to production output.

#FieldJSONTypePresenceConstraints
1resolvedresolvedGetRemoteConfigurationResponsesingular—

ValidateConfiguration

POST /squiddy.v1.AdminService/ValidateConfiguration

ValidateConfiguration checks a candidate config body against the exact same domain validation that gates a publish (US 3.4.1) — structural schema, the typed semantic rules, and the unknown-key check — and returns the issues without writing anything. It exists so the dashboard editor can surface publish-blocking issues inline as the operator types, sharing the server's validation rather than reimplementing a divergent copy client-side. It is read-only and requires config:view; a body that fails validation is reported as issues in the response (valid=false), not as an error, so the caller always receives the full issue list to render.

Errors: internal, invalid_argument, permission_denied, unauthenticated

ValidateConfigurationRequest

Request of ValidateConfiguration.

ValidateConfigurationRequest carries the candidate config body to validate against the domain rules, scoped to a tenant for authorization (config:view). Unlike a write it names no environment/app: validation is a pure function of the body, so the editor can validate a draft it has not persisted (or even selected a target for) yet. The body is required.

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singulargreater than 0
2config_bodyconfigBodygoogle.protobuf.Structsingularrequired

ValidateConfigurationResponse

Response of ValidateConfiguration.

ValidateConfigurationResponse reports whether the body would pass the publish gate. valid is true exactly when issues is empty; issues carries every problem found (structural, semantic and unknown-key), so the editor can highlight them inline. A structurally invalid body yields a single root issue (empty field) describing why it does not decode.

#FieldJSONTypePresenceConstraints
1validvalidboolsingular—
2issuesissuesConfigValidationIssuerepeated—

Types

TenantMembership

TenantMembership is one of the caller's memberships, enriched with the tenant's display fields so the shell can render the tenant switcher without a second round-trip. tenant_id is the tenant the caller belongs to; tenant_slug is its stable handle and tenant_name its display label; role is the caller's role in that tenant (owner/admin/editor/viewer).

#FieldJSONTypePresenceConstraints
1tenant_idtenantIdint64singular—
2tenant_slugtenantSlugstringsingular—
3tenant_nametenantNamestringsingular—
4rolerolestringsingular—
5suspendedsuspendedboolsingular—
6suspension_reasonsuspensionReasonstringsingular—
7serving_blockedservingBlockedboolsingular—
8planplanTenantPlansingular—

OnboardingState

OnboardingState is one organization's position in the first-run quickstart (US 9.2.1). step is the rung reached on a fixed ladder that starts at 1 (the coordinates the organization was born with) and never moves backwards; total_steps is how long that ladder is, sent rather than hardcoded in the client so the two cannot disagree about what "finished" means. dismissed says whether somebody closed the wizard — a decision that can be taken back, which is why it is a fact of its own and not a rung. complete is the verdict the server applies (step has reached the end), sent for the same reason GetOrganizationAllowance sends at_limit: the boundary is a server rule.

A client shows the wizard when it is neither complete nor dismissed. That composition is left to the client because it is a rendering choice; where the ladder ends is not.

#FieldJSONTypePresenceConstraints
1stepstepint32singular—
2total_stepstotalStepsint32singular—
3dismisseddismissedboolsingular—
4completecompleteboolsingular—

Member

Member is one member of a tenant as the members admin screen lists it (US 6.7.1): the identity behind the membership and the role held. user_id is the stable account key the role-change and removal procedures target; subject is the durable OIDC sub; email and name are display claims that may be empty when the provider does not supply them. role is one of owner/admin/editor/viewer. joined_at is when the membership was created, an RFC 3339 timestamp string. last_seen_at is the member's last access — their last OIDC login (US 15.2.1) — as an RFC 3339 timestamp string, empty when the user has never signed in since the stamp was introduced.

#FieldJSONTypePresenceConstraints
1user_iduserIdint64singular—
2subjectsubjectstringsingular—
3emailemailstringsingular—
4namenamestringsingular—
5rolerolestringsingular—
6joined_atjoinedAtstringsingular—
7last_seen_atlastSeenAtstringsingular—

ApiKey

ApiKey is the non-secret metadata view of one data-plane API key as the keys admin screen lists it (US 6.7.2). id is the stable handle the rename/revoke procedures target; environment_id is the (tenant, environment) scope the key authenticates as; name is the human label; prefix is the non-secret leading fragment of the plaintext, letting a user recognise a key without its secret. created_at is when the key was minted; last_used_at is when it last authenticated a request, empty until it has been used; revoked_at is when it was revoked, empty while the key is active. All timestamps are RFC 3339 strings. The plaintext secret is never carried here — it is returned only once, by GenerateApiKey.

#FieldJSONTypePresenceConstraints
1ididint64singular—
2environment_idenvironmentIdint64singular—
3namenamestringsingular—
4prefixprefixstringsingular—
5created_atcreatedAtstringsingular—
6last_used_atlastUsedAtstringsingular—
7revoked_atrevokedAtstringsingular—

Project

Project is the control-plane view of a project — the relational envelope grouping a tenant's apps. Timestamps are RFC 3339 strings, as elsewhere in this service.

#FieldJSONTypePresenceConstraints
1ididint64singular—
2tenant_idtenantIdint64singular—
3slugslugstringsingular—
4namenamestringsingular—
5created_atcreatedAtstringsingular—
6updated_atupdatedAtstringsingular—

Environment

Environment is the control-plane view of a deployment stage of a project. tenant_id is the denormalized owning tenant it is scoped to; name is unique within the project.

#FieldJSONTypePresenceConstraints
1ididint64singular—
2project_idprojectIdint64singular—
3tenant_idtenantIdint64singular—
4namenamestringsingular—
5created_atcreatedAtstringsingular—
6updated_atupdatedAtstringsingular—

Configuration

Configuration is the control-plane view of one app's configuration in an environment: the relational envelope plus the JSON body. version and is_published expose the versioning state; editing a published (immutable) version forks a new draft rather than mutating it (US 3.2.1). published_by and published_at are the publish lineage — who published the version and when — empty while it is an unpublished draft (populated by the publish flow, US 3.2.2). config_body is the arbitrary JSON object validated against domain.ConfigurationModel.

#FieldJSONTypePresenceConstraints
1ididint64singular—
2tenant_idtenantIdint64singular—
3environment_idenvironmentIdint64singular—
4app_idappIdstringsingular—
5versionversionint32singular—
6is_publishedisPublishedboolsingular—
7config_bodyconfigBodygoogle.protobuf.Structsingular—
8created_atcreatedAtstringsingular—
9updated_atupdatedAtstringsingular—
10published_bypublishedBystringsingular—
11published_atpublishedAtstringsingular—

ConfigurationSummary

ConfigurationSummary is the metadata-only view of a configuration returned by the list RPCs (US #599 / M5): the same relational envelope and versioning/publish state as Configuration but without the (up-to-1-MiB) config_body, so a listing stays bounded regardless of body size. Fetch the body on demand with GetConfiguration (current) or GetConfigurationVersion (a specific version). archived_at is the RFC 3339 instant a version was soft-deleted ("" while it is live): the version history includes archived versions (US 6.8.1), and this is what distinguishes them — an archived version can no longer be fetched, rolled back to, or published.

#FieldJSONTypePresenceConstraints
1ididint64singular—
2tenant_idtenantIdint64singular—
3environment_idenvironmentIdint64singular—
4app_idappIdstringsingular—
5versionversionint32singular—
6is_publishedisPublishedboolsingular—
7created_atcreatedAtstringsingular—
8updated_atupdatedAtstringsingular—
9published_bypublishedBystringsingular—
10published_atpublishedAtstringsingular—
11archived_atarchivedAtstringsingular—

AuditLogEntry

AuditLogEntry is one immutable record of a control-plane configuration mutation: who acted (actor_subject is the durable OIDC subject; actor_email the display claim, may be empty), the action (create/update/delete/publish/rollback), the configuration touched (resource_ref encodes environment/app/version), and the before/after diff. diff is a JSON object whose shape depends on the action: a body change is {"old": <body|null>, "new": <body|null>}; a publish is the before/after published-version state. created_at is an RFC 3339 timestamp string.

actor_platform marks an entry recorded for a platform administrator acting on the tenant from outside it — provisioning, suspension, reactivation (US 18.6.1). Those rows live in the affected tenant's own trail, so the customer sees what was decided about them; the flag is what lets a client present them as an outside action rather than as one of their own members'.

#FieldJSONTypePresenceConstraints
1ididint64singular—
2tenant_idtenantIdint64singular—
3actor_subjectactorSubjectstringsingular—
4actor_emailactorEmailstringsingular—
5actionactionstringsingular—
6resource_typeresourceTypestringsingular—
7resource_refresourceRefstringsingular—
8diffdiffgoogle.protobuf.Structsingular—
9created_atcreatedAtstringsingular—
10actor_platformactorPlatformboolsingular—

ConfigValidationIssue

ConfigValidationIssue is one problem found in a candidate body: field locates the offending part (a dotted/indexed path such as "api[0].serverMode" or "languages.defaultLang"; empty for a whole-body structural problem) and message explains, for a human, why it is invalid. It mirrors domain.ConfigIssue so the editor shows the same issues the publish gate would raise.

#FieldJSONTypePresenceConstraints
1fieldfieldstringsingular—
2messagemessagestringsingular—

TenantPlan

TenantPlan is the commercial plan a tenant is on (US 8.1.1) — the single thing its erogation limits are read from. It is an enum on the wire, while the column that stores it is TEXT with a CHECK: the schema keeps the vocabulary changeable without a type migration, and the contract keeps it enumerable, so the back-office renders the plans the server knows instead of a list of its own.

Adding a plan is a backward-compatible extension of this enum. A client one release behind decodes an unknown value as the raw number rather than failing, which is why the plans are named here and nowhere else in the dashboard.

It is declared HERE, on the customer-facing contract, and imported by platform.proto — not the reverse. The plan is a fact about the customer's own tenant: it rides on their TenantMembership so their dashboard can name the plan they are served under (BUG #917), while the back-office channel is only where a plan is chosen. One enum, on the surface whose readers include the customer.

#ValueMeaning
0TENANT_PLAN_UNSPECIFIEDTENANT_PLAN_UNSPECIFIED is the proto3 default and is not a plan: on a request it means "the server's default", and it never appears on a stored tenant.
1TENANT_PLAN_FREETENANT_PLAN_FREE is the entry plan every self-serve organization is born on.
2TENANT_PLAN_PROTENANT_PLAN_PRO is the paid plan.
3TENANT_PLAN_ENTERPRISETENANT_PLAN_ENTERPRISE is the negotiated plan, and the one every tenant that predates plans was backfilled onto: an existing customer must not wake up on a smaller allowance than they had.