tenant

package
v0.0.0-...-64c266f Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 1, 2026 License: MIT Imports: 10 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
var ErrInvalidTenantID = errors.New("invalid tenant id")

ErrInvalidTenantID is returned by ParseID when the input does not match the locked tenant ID grammar.

View Source
var ErrInvalidWSID = errors.New("invalid wsid")

ErrInvalidWSID is returned by ParseWSID when the input is empty or the tenant-prefix component is malformed.

View Source
var ErrTenantExists = errors.New("tenant: already exists")

ErrTenantExists is returned by Create when a row with the same id (or unique key) already exists.

View Source
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

func WithContext

func WithContext(ctx context.Context, id ID) context.Context

WithContext returns a copy of ctx carrying id. FromContext reads it back. Middleware stores the bearer-bound tenant here; handlers read it to enforce tenant-scoped access.

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

func FromContext(ctx context.Context) (ID, bool)

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

func ParseID(s string) (ID, error)

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.

func (ID) Prefix

func (id ID) Prefix() string

Prefix returns the canonical 6-char tenant prefix used as the leading component of a WSID (e.g. "t-abcd" from "t-abcd1234"). Shorter IDs (only the DefaultTenant exception is shorter than 6 chars in normal operation) are returned as-is.

func (ID) String

func (id ID) String() string

String returns the canonical lowercase form.

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.

const (
	KindUser   Kind = "user"
	KindOrg    Kind = "org"
	KindSystem Kind = "system"
)

type ListOptions

type ListOptions struct {
	Limit int
	After ID
}

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) Create

func (m *MemStore) Create(_ context.Context, t Tenant) error

func (*MemStore) Delete

func (m *MemStore) Delete(_ context.Context, id ID) error

Delete implements Store.

func (*MemStore) Get

func (m *MemStore) Get(_ context.Context, id ID) (Tenant, error)

func (*MemStore) GetByGitHub

func (m *MemStore) GetByGitHub(_ context.Context, githubID int64) (Tenant, error)

func (*MemStore) List

func (m *MemStore) List(_ context.Context, opts ListOptions) ([]Tenant, error)

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.

func (*MemStore) Migrate

func (m *MemStore) Migrate(ctx context.Context) error

Migrate seeds DefaultTenant if the store is empty (mirrors SQLStore.Migrate semantics).

func (*MemStore) Update

func (m *MemStore) Update(_ context.Context, id ID, u Update) (Tenant, error)

Update implements Store.

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

func NewSQLStore(db *sql.DB) *SQLStore

NewSQLStore wraps db. The pool's lifecycle is the caller's.

func (*SQLStore) Create

func (s *SQLStore) Create(ctx context.Context, t Tenant) error

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) Delete

func (s *SQLStore) Delete(ctx context.Context, id ID) error

Delete implements Store.

func (*SQLStore) Get

func (s *SQLStore) Get(ctx context.Context, id ID) (Tenant, error)

Get implements Store.

func (*SQLStore) GetByGitHub

func (s *SQLStore) GetByGitHub(ctx context.Context, githubID int64) (Tenant, error)

GetByGitHub implements Store.

func (*SQLStore) List

func (s *SQLStore) List(ctx context.Context, opts ListOptions) ([]Tenant, error)

List implements Store with id-ordered pagination.

func (*SQLStore) Migrate

func (s *SQLStore) Migrate(ctx context.Context) error

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).

func (*SQLStore) Update

func (s *SQLStore) Update(ctx context.Context, id ID, u Update) (Tenant, error)

Update implements Store. The implementation builds a sparse UPDATE so unchanged columns keep their stored value (including NULL).

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

type WSID struct {
	Tenant ID
	Slug   string
}

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

func ParseWSID(s string) (WSID, error)

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

func (w WSID) FullTenant(bearer ID) ID

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.

func (WSID) String

func (w WSID) String() string

String returns the canonical "<prefix>:<slug>" form. When Tenant is DefaultTenant the prefix is "t-defa" (the first 6 chars) — callers that want to round-trip the bare form must compare against Slug directly.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL