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 accountId claim at issuance time. To work with two workspaces concurrently, hold two access tokens and pass them via huudis.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