Workspaces
A workspace is a Huudis tenant — one set of members, one set of OIDC clients, one set of policies. Most accounts own one workspace; multi-brand operators split them. The Go SDK exposes the merchant-facing surface behind client.Workspaces. For the HTTP surface see API → Workspaces; for the data model see Concepts → Workspace.
Field on the Client
client.Workspaces — type *huudis.WorkspacesResource. Shares the parent client's HTTP, issuer, and session.
There's no Delete — workspaces accumulate audit history and webhook subscriptions downstream products rely on. To "stop using" one, revoke its access keys and mark members inactive. Hard delete is a support ticket.
Methods
List
Signature. func (r *WorkspacesResource) List(ctx context.Context, auth RequestAuth) ([]Workspace, error)
Returns every workspace the calling identity can access — usually the one the user belongs to, plus any others they've been added to as a member.
all, err := client.Workspaces.List(ctx, huudis.RequestAuth{})
if err != nil {
return err
}
for _, w := range all {
fmt.Printf("%s: %s [role: %s]\n", w.ID, w.Name, w.Role)
}
Create
Signature. func (r *WorkspacesResource) Create(ctx context.Context, in CreateWorkspaceInput, auth RequestAuth) (*Workspace, error)
Creates a new workspace. The caller becomes the owner. Slug is optional and must be unique across all of Huudis; if you omit it, Huudis derives one from Name.
ws, err := client.Workspaces.Create(ctx, huudis.CreateWorkspaceInput{
Name: "Acme Retail",
Slug: "acme-retail",
}, huudis.RequestAuth{})
if err != nil {
return err
}
log.Println(ws.ID, ws.OwnerID)
The new workspace starts empty — no OIDC clients, no policies, no service accounts. Enable Forjio services individually with client.Services.Enable(...).
Update
Signature. func (r *WorkspacesResource) Update(ctx context.Context, id string, patch map[string]any, auth RequestAuth) (*Workspace, error)
PATCH — only the keys in patch change. Renaming is fine; the workspace ID is the stable handle every other resource references.
_, err := client.Workspaces.Update(ctx, "ws_01HX...", map[string]any{
"name": "Acme Retail Indonesia",
}, huudis.RequestAuth{})
Switch
Signature. func (r *WorkspacesResource) Switch(ctx context.Context, id string, auth RequestAuth) (*SwitchWorkspaceResult, error)
The fulcrum of multi-workspace life. Mints a fresh access token scoped to the target workspace — every subsequent admin call operates under that workspace's data.
res, err := client.Workspaces.Switch(ctx, "ws_01HX...", huudis.RequestAuth{})
if err != nil {
return err
}
// Use res.AccessToken going forward; pass it via RequestAuth or your Session.
Switch is the only way to cross workspaces. A token minted for workspace A can never read workspace B's data, no matter how many policies allow it. The "current workspace" is baked into the JWT's
accountIdclaim at issuance time. To work with two workspaces concurrently, hold two access tokens and pass them viahuudis.RequestAuth{AuthToken: ...}per call.
Types
type Workspace struct {
ID string `json:"id"` // "ws_..."
Name string `json:"name"`
Slug string `json:"slug,omitempty"`
OwnerID string `json:"ownerId"` // "usr_..."
Role string `json:"role,omitempty"` // caller's role: "owner" | "admin" | "member" | "viewer"
CreatedAt string `json:"createdAt"` // ISO-8601
}
type SwitchWorkspaceResult struct {
AccessToken string `json:"accessToken"`
}
See API → Workspaces for field-level rules.
Common patterns
Pick the workspace at startup
func pickWorkspace(ctx context.Context, c *huudis.Client, prefer string) (*huudis.Workspace, error) {
all, err := c.Workspaces.List(ctx, huudis.RequestAuth{})
if err != nil {
return nil, err
}
if prefer != "" {
for i := range all {
if all[i].ID == prefer || all[i].Slug == prefer {
return &all[i], nil
}
}
}
if len(all) == 0 {
return nil, errors.New("no workspaces visible")
}
return &all[0], nil
}
Provision a new brand
func launchBrand(ctx context.Context, c *huudis.Client, name, ownerEmail string) (*huudis.Workspace, error) {
ws, err := c.Workspaces.Create(ctx, huudis.CreateWorkspaceInput{Name: name}, huudis.RequestAuth{})
if err != nil {
return nil, err
}
sw, err := c.Workspaces.Switch(ctx, ws.ID, huudis.RequestAuth{})
if err != nil {
return nil, err
}
auth := huudis.RequestAuth{AuthToken: sw.AccessToken}
if _, err := c.IAM.SendInvite(ctx, huudis.SendInviteInput{
Email: ownerEmail, Role: "owner",
}, auth); err != nil {
return nil, err
}
if _, err := c.OidcClients.Create(ctx, huudis.CreateOidcClientInput{
Name: name + " portal",
RedirectUris: []string{"https://" + slug(name) + ".example.com/callback"},
}, auth); err != nil {
return nil, err
}
return ws, nil
}
Audit access
all, _ := client.Workspaces.List(ctx, huudis.RequestAuth{})
for _, w := range all {
t, _ := time.Parse(time.RFC3339, w.CreatedAt)
age := int(time.Since(t).Hours() / 24)
fmt.Println(w.ID, w.Name, w.Role, fmt.Sprintf("%dd", age))
}
Cancellation-aware lookups
ctx, cancel := context.WithTimeout(parentCtx, 2*time.Second)
defer cancel()
all, err := client.Workspaces.List(ctx, huudis.RequestAuth{})
If the parent context is canceled, the SDK aborts the in-flight request and returns a wrapped context error.
Errors
Code |
Status |
Cause |
|---|---|---|
validation_error |
400 | Bad Slug (already taken, illegal chars), name too long. |
not_found |
404 | Workspace ID isn't visible to the caller. |
forbidden |
403 | Caller's role can't perform the action (e.g. viewer calling Update). |
state_conflict |
409 | Tried to demote the last owner; tried to switch to a disabled workspace. |
Branch with errors.As:
var he *huudis.Error
if errors.As(err, &he) && he.Code == "forbidden" {
log.Println("caller lacks workspace admin scope")
}
Next
- Users — manage members of a workspace.
- OIDC clients — let your app delegate auth to this workspace.
- Service accounts — provision non-human principals.
- API → Workspaces — HTTP-level reference.