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
| Procedure | Summary |
|---|---|
WhoAmI | WhoAmI returns the identity of the authenticated principal making the call. |
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). |
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. |
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. |
GetEmailVerification | GetEmailVerification reports whether the authenticated caller has proved they own the email address on their account (US 9.1.5). |
ResendEmailVerification | ResendEmailVerification asks the identity provider to send the authenticated caller a fresh verification message (US 9.1.5). |
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. |
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). |
UpdateOnboarding | UpdateOnboarding records how far the organization has got and whether the wizard is put away (US 9.2.1). |
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. |
UpdateMemberRole | UpdateMemberRole changes an existing member's role within a tenant and returns the updated member. |
RemoveMember | RemoveMember removes a member from a tenant. |
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. |
RenameApiKey | RenameApiKey changes the display name of an existing key the caller manages (US 2.2.1). |
RevokeApiKey | RevokeApiKey revokes a key so it can no longer authenticate a request (US 2.2.1). |
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). |
CreateProject | CreateProject creates a project (a group of apps) under a tenant and returns it. |
GetProject | GetProject returns a single project by id, scoped to the caller's tenant. |
ListProjects | ListProjects returns every project of the tenant, ordered by id. |
UpdateProject | UpdateProject changes a project's display name (the slug is its stable handle and is immutable). |
DeleteProject | DeleteProject deletes an empty project. |
CreateEnvironment | CreateEnvironment creates a deployment stage (e.g. dev/qa/prod) under a project and returns it. |
GetEnvironment | GetEnvironment returns a single environment by id, scoped to the caller's tenant. |
ListEnvironments | ListEnvironments returns every environment of a project, ordered by id. |
UpdateEnvironment | UpdateEnvironment changes an environment's name, unique within its project. |
DeleteEnvironment | DeleteEnvironment deletes an empty environment. |
CreateConfiguration | CreateConfiguration creates the draft configuration for (environment, app_id) and returns it. |
GetConfiguration | GetConfiguration returns the current (non-archived) configuration for (environment, app_id). |
ListConfigurations | ListConfigurations returns every current (non-archived) configuration in an environment, ordered by app_id. |
UpdateConfiguration | UpdateConfiguration records the new body as the current working configuration for (environment, app_id), validating it first. |
DeleteConfiguration | DeleteConfiguration archives (soft-deletes) the current configuration for (environment, app_id), preserving history. |
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. |
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. |
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. |
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). |
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. |
PreviewResolve | PreviewResolve simulates what a client with the given context would receive for a configuration, before it is published. |
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. |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | subject | subject | string | singular | — |
| 2 | email | email | string | singular | — |
| 3 | name | name | string | singular | — |
| 4 | is_platform_admin | isPlatformAdmin | bool | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | memberships | memberships | TenantMembership | repeated | — |
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).
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
| 2 | email | email | string | singular | at most 254 characters; a valid email address |
| 3 | role | role | string | singular | one of owner, admin, editor, viewer |
| 4 | name | name | string | singular | at most 200 characters |
| 5 | language | language | string | singular | one of "", it, en |
| 6 | resend | resend | bool | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | member | member | Member | singular | — |
| 2 | email_sent | emailSent | bool | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | name | name | string | singular | at 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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | membership | membership | TenantMembership | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | email | email | string | singular | — |
| 2 | verified | verified | bool | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | sent | sent | bool | singular | — |
| 2 | already_verified | alreadyVerified | bool | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | owned | owned | int32 | singular | — |
| 2 | limit | limit | int32 | singular | — |
| 3 | at_limit | atLimit | bool | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
GetOnboardingResponse
Response of GetOnboarding.
GetOnboardingResponse carries the organization's quickstart state.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | state | state | OnboardingState | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
| 2 | step | step | int32 | singular | at least 1 |
| 3 | dismissed | dismissed | bool | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | state | state | OnboardingState | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
ListMembersResponse
Response of ListMembers.
ListMembersResponse returns the tenant's members, ordered by user id.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | members | members | Member | repeated | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
| 2 | user_id | userId | int64 | singular | greater than 0 |
| 3 | role | role | string | singular | one of owner, admin, editor, viewer |
UpdateMemberRoleResponse
Response of UpdateMemberRole.
UpdateMemberRoleResponse returns the member with its new role.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | member | member | Member | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
| 2 | user_id | userId | int64 | singular | greater than 0 |
RemoveMemberResponse
Response of RemoveMember.
RemoveMemberResponse echoes the user id of the removed member.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | user_id | userId | int64 | singular | — |
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.)
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
| 2 | environment_id | environmentId | int64 | singular | greater than 0 |
| 3 | name | name | string | singular | at 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).
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | api_key | apiKey | string | singular | — |
| 2 | id | id | int64 | singular | — |
| 3 | prefix | prefix | string | singular | — |
| 4 | name | name | string | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
| 2 | id | id | int64 | singular | greater than 0 |
| 3 | name | name | string | singular | at 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).
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | id | id | int64 | singular | — |
| 2 | name | name | string | singular | — |
| 3 | prefix | prefix | string | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
| 2 | id | id | int64 | singular | greater 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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | id | id | int64 | singular | — |
| 2 | revoked_at | revokedAt | string | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater 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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | api_keys | apiKeys | ApiKey | repeated | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
| 2 | slug | slug | string | singular | at least 1 characters; at most 63 characters; matches ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$; ignore = 1 |
| 3 | name | name | string | singular | at least 1 characters; at most 200 characters |
CreateProjectResponse
Response of CreateProject.
CreateProjectResponse returns the created project.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | project | project | Project | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
| 2 | id | id | int64 | singular | greater than 0 |
GetProjectResponse
Response of GetProject.
GetProjectResponse returns the requested project.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | project | project | Project | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
ListProjectsResponse
Response of ListProjects.
ListProjectsResponse returns the tenant's projects, ordered by id.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | projects | projects | Project | repeated | — |
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).
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
| 2 | id | id | int64 | singular | greater than 0 |
| 3 | name | name | string | singular | at least 1 characters; at most 200 characters |
UpdateProjectResponse
Response of UpdateProject.
UpdateProjectResponse returns the updated project.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | project | project | Project | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
| 2 | id | id | int64 | singular | greater than 0 |
DeleteProjectResponse
Response of DeleteProject.
DeleteProjectResponse echoes the id of the deleted project.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | id | id | int64 | singular | — |
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).
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
| 2 | project_id | projectId | int64 | singular | greater than 0 |
| 3 | name | name | string | singular | at least 1 characters; at most 63 characters |
CreateEnvironmentResponse
Response of CreateEnvironment.
CreateEnvironmentResponse returns the created environment.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | environment | environment | Environment | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
| 2 | id | id | int64 | singular | greater than 0 |
GetEnvironmentResponse
Response of GetEnvironment.
GetEnvironmentResponse returns the requested environment.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | environment | environment | Environment | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
| 2 | project_id | projectId | int64 | singular | greater than 0 |
ListEnvironmentsResponse
Response of ListEnvironments.
ListEnvironmentsResponse returns the project's environments, ordered by id.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | environments | environments | Environment | repeated | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
| 2 | id | id | int64 | singular | greater than 0 |
| 3 | name | name | string | singular | at least 1 characters; at most 63 characters |
UpdateEnvironmentResponse
Response of UpdateEnvironment.
UpdateEnvironmentResponse returns the updated environment.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | environment | environment | Environment | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
| 2 | id | id | int64 | singular | greater than 0 |
DeleteEnvironmentResponse
Response of DeleteEnvironment.
DeleteEnvironmentResponse echoes the id of the deleted environment.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | id | id | int64 | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
| 2 | environment_id | environmentId | int64 | singular | greater than 0 |
| 3 | app_id | appId | string | singular | at least 1 characters; at most 200 characters |
| 4 | config_body | configBody | google.protobuf.Struct | singular | required |
CreateConfigurationResponse
Response of CreateConfiguration.
CreateConfigurationResponse returns the created draft configuration.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | configuration | configuration | Configuration | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
| 2 | environment_id | environmentId | int64 | singular | greater than 0 |
| 3 | app_id | appId | string | singular | at least 1 characters; at most 200 characters |
GetConfigurationResponse
Response of GetConfiguration.
GetConfigurationResponse returns the requested configuration.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | configuration | configuration | Configuration | singular | — |
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).
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
| 2 | environment_id | environmentId | int64 | singular | greater than 0 |
| 3 | page_size | pageSize | int32 | singular | at least 0 |
| 4 | page_token | pageToken | string | singular | at 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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | configurations | configurations | ConfigurationSummary | repeated | — |
| 2 | next_page_token | nextPageToken | string | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
| 2 | environment_id | environmentId | int64 | singular | greater than 0 |
| 3 | app_id | appId | string | singular | at least 1 characters; at most 200 characters |
| 4 | config_body | configBody | google.protobuf.Struct | singular | required |
UpdateConfigurationResponse
Response of UpdateConfiguration.
UpdateConfigurationResponse returns the updated configuration.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | configuration | configuration | Configuration | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
| 2 | environment_id | environmentId | int64 | singular | greater than 0 |
| 3 | app_id | appId | string | singular | at least 1 characters; at most 200 characters |
DeleteConfigurationResponse
Response of DeleteConfiguration.
DeleteConfigurationResponse echoes the id of the archived configuration.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | id | id | int64 | singular | — |
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).
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
| 2 | environment_id | environmentId | int64 | singular | greater than 0 |
| 3 | app_id | appId | string | singular | at least 1 characters; at most 200 characters |
| 4 | page_size | pageSize | int32 | singular | at least 0 |
| 5 | page_token | pageToken | string | singular | at 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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | versions | versions | ConfigurationSummary | repeated | — |
| 2 | next_page_token | nextPageToken | string | singular | — |
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).
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
| 2 | environment_id | environmentId | int64 | singular | greater than 0 |
| 3 | app_id | appId | string | singular | at least 1 characters; at most 200 characters |
| 4 | version | version | int32 | singular | greater than 0 |
GetConfigurationVersionResponse
Response of GetConfigurationVersion.
GetConfigurationVersionResponse returns the requested version, body included.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | configuration | configuration | Configuration | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
| 2 | environment_id | environmentId | int64 | singular | greater than 0 |
| 3 | app_id | appId | string | singular | at least 1 characters; at most 200 characters |
| 4 | version | version | int32 | singular | greater than 0 |
RollbackConfigurationResponse
Response of RollbackConfiguration.
RollbackConfigurationResponse returns the resulting working configuration (a mutated draft or a freshly forked draft).
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | configuration | configuration | Configuration | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
| 2 | environment_id | environmentId | int64 | singular | greater than 0 |
| 3 | app_id | appId | string | singular | at least 1 characters; at most 200 characters |
| 4 | version | version | int32 | singular | greater than 0 |
PublishConfigurationResponse
Response of PublishConfiguration.
PublishConfigurationResponse returns the now-published configuration, carrying the publish lineage (published_by / published_at) the transition stamped.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | configuration | configuration | Configuration | singular | — |
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).
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
| 2 | page_size | pageSize | int32 | singular | at least 0 |
| 3 | page_token | pageToken | string | singular | at 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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | entries | entries | AuditLogEntry | repeated | — |
| 2 | next_page_token | nextPageToken | string | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
| 2 | environment_id | environmentId | int64 | singular | greater than 0 |
| 3 | app_id | appId | string | singular | at least 1 characters; at most 200 characters |
| 4 | version | version | int32 | singular | at least 0 |
| 5 | app_version | appVersion | string | singular | at least 1 characters; at most 50 characters |
| 6 | app_type | appType | string | singular | at least 1 characters; at most 50 characters |
| 7 | app_mode | appMode | string | singular | at least 1 characters; at most 50 characters |
| 8 | language | language | string | singular | at least 1 characters; at most 50 characters |
| 9 | custom_params | customParams | string | repeated | — |
| 10 | entity_id | entityId | string | singular | at 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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | resolved | resolved | GetRemoteConfigurationResponse | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | greater than 0 |
| 2 | config_body | configBody | google.protobuf.Struct | singular | required |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | valid | valid | bool | singular | — |
| 2 | issues | issues | ConfigValidationIssue | repeated | — |
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).
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | tenant_id | tenantId | int64 | singular | — |
| 2 | tenant_slug | tenantSlug | string | singular | — |
| 3 | tenant_name | tenantName | string | singular | — |
| 4 | role | role | string | singular | — |
| 5 | suspended | suspended | bool | singular | — |
| 6 | suspension_reason | suspensionReason | string | singular | — |
| 7 | serving_blocked | servingBlocked | bool | singular | — |
| 8 | plan | plan | TenantPlan | singular | — |
suspended— suspended reports whether this membership's tenant is currently suspended (US 18.5.5). It is the state predicate, derived server-side from the stored timestamp exactly as the back-office view is (domain.Tenant.IsActive), so no client re-implements the derivation. Every tenant-scoped RPC on a suspended tenant is refused (US 18.5.3); the dashboard reads this field to explain that once, on a screen of its own, instead of letting the caller discover it one failed call at a time. A caller whose memberships are ALL suspended is shown that screen rather than the shell.suspension_reason— suspension_reason is the motivation the platform administrator recorded, empty on an active tenant. It is the same text the back-office shows (US 18.5.4) and it is deliberately shown to the customer: it is the answer to "what happened", which is the whole point of the screen. The record's other two facts — when, and which administrator decided — stay in the back-office view: neither helps the customer act, and the author's identity is internal.serving_blocked— serving_blocked reports whether the suspension also stopped this tenant's DATA plane (US 18.7.1): whether its apps are being refused their configuration, not only its administrators locked out. It is shown to the customer because the alternative is worse than saying nothing: the screen's copy states that published configuration keeps being served, which is true of an ordinary suspension and a falsehood at the worst possible moment for a customer whose apps HAVE stopped. Being told plainly that serving stopped is what lets them understand an outage they are already living through.plan— plan is the commercial plan this membership's tenant is on (US 8.1.1). It rides on the membership for the same reason the display fields do: the shell already reads this list to draw its sidebar, and the plan is one of the facts that sidebar states — so naming it costs no second round-trip. It is here because the plan is the customer's own fact and not only the back-office's: since US 8.1.1 it governs the limits their apps are served under, and until then the dashboard's plan card was a fixed literal telling every tenant they were on Pro (BUG #917). A stored tenant always carries one of the named plans, so TENANT_PLAN_UNSPECIFIED here means only "this server did not report a plan" — an older release, or a read that failed — and the dashboard shows that as unknown rather than as the entry plan, which would be a claim about a customer that nobody made. What the plan ALLOWS is deliberately absent, exactly as on TenantOverview: the limits are declared once, server-side, and a number sent here could be rendered after the server stopped enforcing it.
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | step | step | int32 | singular | — |
| 2 | total_steps | totalSteps | int32 | singular | — |
| 3 | dismissed | dismissed | bool | singular | — |
| 4 | complete | complete | bool | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | user_id | userId | int64 | singular | — |
| 2 | subject | subject | string | singular | — |
| 3 | email | email | string | singular | — |
| 4 | name | name | string | singular | — |
| 5 | role | role | string | singular | — |
| 6 | joined_at | joinedAt | string | singular | — |
| 7 | last_seen_at | lastSeenAt | string | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | id | id | int64 | singular | — |
| 2 | environment_id | environmentId | int64 | singular | — |
| 3 | name | name | string | singular | — |
| 4 | prefix | prefix | string | singular | — |
| 5 | created_at | createdAt | string | singular | — |
| 6 | last_used_at | lastUsedAt | string | singular | — |
| 7 | revoked_at | revokedAt | string | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | id | id | int64 | singular | — |
| 2 | tenant_id | tenantId | int64 | singular | — |
| 3 | slug | slug | string | singular | — |
| 4 | name | name | string | singular | — |
| 5 | created_at | createdAt | string | singular | — |
| 6 | updated_at | updatedAt | string | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | id | id | int64 | singular | — |
| 2 | project_id | projectId | int64 | singular | — |
| 3 | tenant_id | tenantId | int64 | singular | — |
| 4 | name | name | string | singular | — |
| 5 | created_at | createdAt | string | singular | — |
| 6 | updated_at | updatedAt | string | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | id | id | int64 | singular | — |
| 2 | tenant_id | tenantId | int64 | singular | — |
| 3 | environment_id | environmentId | int64 | singular | — |
| 4 | app_id | appId | string | singular | — |
| 5 | version | version | int32 | singular | — |
| 6 | is_published | isPublished | bool | singular | — |
| 7 | config_body | configBody | google.protobuf.Struct | singular | — |
| 8 | created_at | createdAt | string | singular | — |
| 9 | updated_at | updatedAt | string | singular | — |
| 10 | published_by | publishedBy | string | singular | — |
| 11 | published_at | publishedAt | string | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | id | id | int64 | singular | — |
| 2 | tenant_id | tenantId | int64 | singular | — |
| 3 | environment_id | environmentId | int64 | singular | — |
| 4 | app_id | appId | string | singular | — |
| 5 | version | version | int32 | singular | — |
| 6 | is_published | isPublished | bool | singular | — |
| 7 | created_at | createdAt | string | singular | — |
| 8 | updated_at | updatedAt | string | singular | — |
| 9 | published_by | publishedBy | string | singular | — |
| 10 | published_at | publishedAt | string | singular | — |
| 11 | archived_at | archivedAt | string | singular | — |
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'.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | id | id | int64 | singular | — |
| 2 | tenant_id | tenantId | int64 | singular | — |
| 3 | actor_subject | actorSubject | string | singular | — |
| 4 | actor_email | actorEmail | string | singular | — |
| 5 | action | action | string | singular | — |
| 6 | resource_type | resourceType | string | singular | — |
| 7 | resource_ref | resourceRef | string | singular | — |
| 8 | diff | diff | google.protobuf.Struct | singular | — |
| 9 | created_at | createdAt | string | singular | — |
| 10 | actor_platform | actorPlatform | bool | singular | — |
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.
| # | Field | JSON | Type | Presence | Constraints |
|---|---|---|---|---|---|
| 1 | field | field | string | singular | — |
| 2 | message | message | string | singular | — |
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.
| # | Value | Meaning |
|---|---|---|
| 0 | TENANT_PLAN_UNSPECIFIED | TENANT_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. |
| 1 | TENANT_PLAN_FREE | TENANT_PLAN_FREE is the entry plan every self-serve organization is born on. |
| 2 | TENANT_PLAN_PRO | TENANT_PLAN_PRO is the paid plan. |
| 3 | TENANT_PLAN_ENTERPRISE | TENANT_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. |