Documentation
¶
Overview ¶
Package tenant defines the canonical tenant + workspace identifier types for the multi-tenant control plane (gm-o9t8.3.9). The model is locked:
- Tenant ID format: t-<8 base32 chars> (lowercase). DefaultTenant ("t-default") is the well-known constant used for single-user installs and as the backwards-compat fallback when a bare wsid is parsed.
- WSID format: <tenant-prefix>:<slug>, where tenant-prefix is the first 6 chars of the tenant ID (e.g. "t-abcd:my-project"). A wsid without a colon is parsed as "t-default:<wsid>" so M1 routes keep working unchanged.
The package is intentionally self-contained — no dependency on the server/auth/dolt layers — so tests, CLI tooling and middleware can all agree on the same parsing rules.
Index ¶
- Variables
- func WithContext(ctx context.Context, id ID) context.Context
- type ID
- type Kind
- type ListOptions
- type MemStore
- func (m *MemStore) Create(_ context.Context, t Tenant) error
- func (m *MemStore) Delete(_ context.Context, id ID) error
- func (m *MemStore) Get(_ context.Context, id ID) (Tenant, error)
- func (m *MemStore) GetByGitHub(_ context.Context, githubID int64) (Tenant, error)
- func (m *MemStore) List(_ context.Context, opts ListOptions) ([]Tenant, error)
- func (m *MemStore) Migrate(ctx context.Context) error
- func (m *MemStore) Update(_ context.Context, id ID, u Update) (Tenant, error)
- type SQLStore
- func (s *SQLStore) Create(ctx context.Context, t Tenant) error
- func (s *SQLStore) Delete(ctx context.Context, id ID) error
- func (s *SQLStore) Get(ctx context.Context, id ID) (Tenant, error)
- func (s *SQLStore) GetByGitHub(ctx context.Context, githubID int64) (Tenant, error)
- func (s *SQLStore) List(ctx context.Context, opts ListOptions) ([]Tenant, error)
- func (s *SQLStore) Migrate(ctx context.Context) error
- func (s *SQLStore) Update(ctx context.Context, id ID, u Update) (Tenant, error)
- type Store
- type Tenant
- type Update
- type WSID
Constants ¶
This section is empty.
Variables ¶
var ErrInvalidTenantID = errors.New("invalid tenant id")
ErrInvalidTenantID is returned by ParseID when the input does not match the locked tenant ID grammar.
var ErrInvalidWSID = errors.New("invalid wsid")
ErrInvalidWSID is returned by ParseWSID when the input is empty or the tenant-prefix component is malformed.
var ErrTenantExists = errors.New("tenant: already exists")
ErrTenantExists is returned by Create when a row with the same id (or unique key) already exists.
var ErrTenantNotFound = errors.New("tenant: not found")
ErrTenantNotFound is returned by Get / GetByGitHub when no row matches. Callers use errors.Is to distinguish from infra errors.
Functions ¶
Types ¶
type ID ¶
type ID string
ID is a tenant identifier. Always lowercase; always begins with "t-".
const DefaultTenant ID = "t-default"
DefaultTenant is the well-known single-user / M1 backwards-compat tenant. Every install seeds this row on first boot; bare wsids (without the "<prefix>:" qualifier) resolve to it.
func FromContext ¶
FromContext returns the tenant ID previously stored via WithContext. The second return is false when no tenant was attached.
func NewID ¶
func NewID() ID
NewID generates a fresh tenant ID: 5 random bytes → base32 → lowercase → first 8 chars, prefixed with "t-". 5 random bytes give 40 bits of entropy, which base32-encodes to exactly 8 chars (no padding), comfortably above the birthday-collision threshold for the expected tenant population.
func ParseID ¶
ParseID validates s as a tenant ID. It accepts:
- the literal "t-default", and
- "t-<8 base32 lowercase>" (RFC 4648 alphabet, lowercased).
Anything else is rejected with ErrInvalidTenantID.
type Kind ¶
type Kind string
Kind is the tenant kind enum mirrored from the Dolt schema. Three values are pinned at this stage; the OAuth integration story (gm-o9t8.3.5) is what differentiates user vs org rows on insert.
type ListOptions ¶
ListOptions controls Store.List pagination. Limit clamps page size (zero / negative → store default, currently 50). After is an exclusive lower bound on tenant id; the empty string returns the first page.
type MemStore ¶
type MemStore struct {
// contains filtered or unexported fields
}
MemStore is a process-local Store implementation. Used by tests and by the single-user fallback when --dolt-url is not configured — every request resolves to DefaultTenant, but handlers can still dereference a Store for metadata.
func NewMemStore ¶
func NewMemStore() *MemStore
NewMemStore returns an empty in-memory store. Callers typically call Migrate to seed DefaultTenant before serving.
func (*MemStore) GetByGitHub ¶
func (*MemStore) List ¶
List returns tenants ordered by id ascending. We sort a snapshot of the keys rather than relying on insertion order so the pagination "after" cursor is meaningful for callers that interleave Create with List.
type SQLStore ¶
type SQLStore struct {
// contains filtered or unexported fields
}
SQLStore is the production Store backed by the same Dolt SQL pool the WorkPlane adaptor uses. It owns the `tenants` table; the bd adaptor's schema_migrations table is untouched.
func NewSQLStore ¶
NewSQLStore wraps db. The pool's lifecycle is the caller's.
func (*SQLStore) Create ¶
Create implements Store. github_login / github_id are inserted as NULL when the pointers are nil; the unique constraints then permit multiple null rows (MySQL semantics).
func (*SQLStore) GetByGitHub ¶
GetByGitHub implements Store.
func (*SQLStore) Migrate ¶
Migrate ensures the tenants + auth_tokens tables exist and seeds the DefaultTenant row if the tenants table is empty. Safe to call on every server boot.
Seeding policy: we only insert DefaultTenant when the table has no rows. Operators upgrading from M1 get the seed automatically; fresh installs land with a single 'system'-kind row; multi-tenant rigs add real tenants via the OAuth flow (gm-o9t8.3.5).
type Store ¶
type Store interface {
// Get returns the row for id or ErrTenantNotFound.
Get(ctx context.Context, id ID) (Tenant, error)
// Create inserts t. The row's CreatedAt is set by the store on
// success (callers may leave it zero on input).
Create(ctx context.Context, t Tenant) error
// GetByGitHub returns the tenant linked to the given GitHub
// numeric id, or ErrTenantNotFound. Used during OAuth callback
// (gm-o9t8.3.5) and by the bearer → tenant resolution stub.
GetByGitHub(ctx context.Context, githubID int64) (Tenant, error)
// List returns tenants ordered by id ascending, honouring the
// pagination knobs in opts. Admin-only consumers; the full list
// is expected to be small (operator + a handful of orgs) for the
// foreseeable future, but the page primitive future-proofs the
// shape against larger SaaS deployments.
List(ctx context.Context, opts ListOptions) ([]Tenant, error)
// Update mutates the row for id with the non-nil fields in u.
// Returns ErrTenantNotFound when the row is absent. The
// DefaultTenant row is mutable through this surface (operators
// may need to link a GitHub identity to it post-OAuth).
Update(ctx context.Context, id ID, u Update) (Tenant, error)
// Delete removes the row for id. Returns ErrTenantNotFound when
// the row is absent. Callers are responsible for any cross-row
// referential checks (workspaces-still-exist, etc.).
Delete(ctx context.Context, id ID) error
}
Store is the persistence contract for tenants. The full OAuth surface (link/unlink, GitHub-id rotation) lands in gm-o9t8.3.5; the gm-o9t8.3.9 foundation only needs read access, an insert for the default-tenant seed, and a list for the (admin-only) GET tenant metadata route.
type Tenant ¶
type Tenant struct {
ID ID
GitHubLogin *string
GitHubID *int64
Kind Kind
Tier string
CreatedAt time.Time
}
Tenant is one row from the `tenants` table.
GitHubLogin / GitHubID are nullable: the DefaultTenant row carries nulls, and pre-OAuth seeded rows may also lack a GitHub identity. Tier is the customer subscription level (gm-o9t8.4.2.1); the column defaults to "free" via the schema, so old rows that pre-date the migration appear as free-tier on read.
type Update ¶
type Update struct {
GitHubLogin *string
// Tier is the new subscription level. Nil means leave unchanged;
// non-nil replaces the stored value. Validation against the
// canonical tier enumeration lives in the quota package — Store
// callers are expected to feed already-validated values here.
Tier *string
}
Update carries the mutable fields a PATCH /tenants/{tid} call may touch. Nil pointers mean "leave unchanged"; non-nil pointers replace the stored value. Only github_login is mutable today; kind / github_id rotation lands with the OAuth story (gm-o9t8.3.5).
type WSID ¶
WSID is a parsed workspace identifier. Tenant carries the 6-char tenant prefix as it appears in the wsid (e.g. "t-abcd" or "t-defa"). It is NOT a full tenant ID — middleware that needs to compare against the bearer-bound tenant must compare against bearerTenant.Prefix(), not against bearerTenant directly. Slug carries the per-tenant project slug.
The reason WSID stores the prefix and not the full ID: the wire format only carries the prefix (8 random bits of the body are elided), so the parser cannot reconstruct the full ID without a store lookup. Keeping the prefix here makes String() a faithful inverse of ParseWSID.
func ParseWSID ¶
ParseWSID accepts both the canonical "<tenant-prefix>:<slug>" form and the M1 backwards-compat bare "<slug>" form. The bare form maps to DefaultTenant.
Validation rules:
- Empty input → ErrInvalidWSID.
- Canonical form: the prefix must look like "t-" followed by at least one base32 char; the slug must be non-empty.
- Bare form: the entire input becomes the slug; tenant is DefaultTenant.
ParseWSID does NOT verify that the prefix matches a real tenant — that's the middleware's job (it checks against the bearer-bound tenant). The parser only enforces shape so callers can rely on a well-formed WSID.
func (WSID) FullTenant ¶
FullTenant resolves WSID.Tenant against the bearer-bound tenant id. Returns the bearer-bound full ID iff bearer.Prefix() matches the wsid prefix; otherwise returns the empty ID. Use this in handlers that need the full tenant id (for store lookups, audit logs, etc.) while still enforcing the prefix-match guard.