Realms & Projects
Section titled “Realms & Projects”Multi-tenancy on most platforms is a permission problem: every resource is visible, and nested IAM policies decide who may touch what. Scope one policy wrong and a CI token can delete production. Hoody splits the problem across two primitives.
Projects organize your resources: containers, quotas, team permissions. They work like folders for your computers.
Realms isolate API visibility. A token restricted to a realm cannot see resources outside it. The API does not answer “permission denied” or “unauthorized”; the resources are absent from every response.
Projects
Section titled “Projects”A project is a boundary for containers. Every container belongs to exactly one project: you create every container inside a project, filter container listings by project, and share access with teammates at the project level. Projects give you:
- Container grouping:
frontend,backend,ml-pipeline,staging - Team permissions:
read,edit,deleteper member - Quotas: container limits per project (
max_containers) - Billing: costs tracked per project
# Create a projecthoody projects create --alias "production-api"
# List your projectshoody projects list
# Create a container inside the projecthoody containers create --project $PROJECT_ID \ --server-id $SERVER_ID \ --name "api-server" \ --hoody-kitimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
// Create a projectconst project = await client.api.projects.create({ alias: 'production-api'});
// List all projectsconst projects = await client.api.projects.list();
// Create a container in that projectconst container = await client.api.containers.create( project.data.id, { server_id: SERVER_ID, name: 'api-server', hoody_kit: true });# Create a projectcurl -X POST "https://api.hoody.com/api/v1/projects/" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"alias": "production-api"}'
# List projectscurl "https://api.hoody.com/api/v1/projects/" \ -H "Authorization: Bearer $HOODY_TOKEN"
# Create a container in the projectcurl -X POST "https://api.hoody.com/api/v1/projects/$PROJECT_ID/containers" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "server_id": "'"$SERVER_ID"'", "name": "api-server", "hoody_kit": true }'One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Each call as a single GET link. All three route through an existing container’s curl-1 service, so the first project and container still need the CLI or SDK tab; after that, these links can create more.
# Create project
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/projects/&method=POST&bearer_token=TOKEN&json={"alias":"production-api"}&response=transparent
# List projects
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/projects/&method=GET&bearer_token=TOKEN&response=transparent
# Create container
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/projects/PROJECT_ID/containers&method=POST&bearer_token=TOKEN&json={"server_id":"SERVER_ID","name":"api-server","hoody_kit":true}&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
Realms
Section titled “Realms”Every multi-tenant system has to stop a token from seeing resources it should not see. The usual answer is permission checks: the resource exists, and the token is denied access to it. A misconfigured permission can therefore expose everything; one overly permissive role or one leaked token puts the whole account on the table.
A realm does not restrict access to resources. It filters them out of the API: to a realm-scoped token, out-of-realm resources appear in no response.
How realms work
Section titled “How realms work”A realm is a 24-hex identifier (e.g., 507f1f77bcf86cd799439011) that scopes API visibility. Resources carry a realm_ids: string[] field, auth tokens can be restricted to specific realms, and the API host itself carries the realm scope:
Unscoped: https://api.hoody.comRealm-scoped: https://507f1f77bcf86cd799439011.api.hoody.comWhen you call a realm-scoped host:
- Read operations return only resources whose
realm_idsincludes that realm - Write operations automatically merge the realm into
realm_idson the created resource
When a realm-restricted token calls the unscoped host, the API rejects the request. Such a token can only operate through a realm-scoped host.
Realm-scoped API calls
Section titled “Realm-scoped API calls”# List containers visible in a specific realmhoody --base-url "https://507f1f77bcf86cd799439011.api.hoody.com" \ containers list
# Create a project in a realm (auto-assigned realm_ids)hoody --base-url "https://507f1f77bcf86cd799439011.api.hoody.com" \ projects create --alias "prod-services"
# Discover your token's realm restrictionshoody auth get-currentimport { HoodyClient } from 'hoody-sdk';
// A realm-scoped client sees only resources in this realmconst realmClient = new HoodyClient({ baseURL: 'https://507f1f77bcf86cd799439011.api.hoody.com', token: process.env.HOODY_TOKEN});
// This only returns containers assigned to realm 507f1f77bcf86cd799439011const containers = await realmClient.api.containers.list();
// Projects created here auto-inherit the realmconst project = await realmClient.api.projects.create({ alias: 'prod-services'});// project.data.realm_ids includes '507f1f77bcf86cd799439011'# List containers in a realmcurl "https://507f1f77bcf86cd799439011.api.hoody.com/api/v1/containers/" \ -H "Authorization: Bearer $HOODY_TOKEN"
# Create a project scoped to a realmcurl -X POST "https://507f1f77bcf86cd799439011.api.hoody.com/api/v1/projects/" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"alias": "prod-services"}'
# Discover token realm restrictionscurl "https://api.hoody.com/api/v1/auth/tokens/me" \ -H "Authorization: Bearer $HOODY_TOKEN"One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Each call as a single GET link. The first two go through the realm-scoped host, so they only ever touch resources in realm 507f1f77bcf86cd799439011; the third checks the token itself on the unscoped host.
# List containers in realm
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://507f1f77bcf86cd799439011.api.hoody.com/api/v1/containers/&method=GET&bearer_token=TOKEN&response=transparent
# Create project in realm
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://507f1f77bcf86cd799439011.api.hoody.com/api/v1/projects/&method=POST&bearer_token=TOKEN&json={"alias":"prod-services"}&response=transparent
# Discover token realm restrictions
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/auth/tokens/me&method=GET&bearer_token=TOKEN&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
Projects and realms together
Section titled “Projects and realms together”Projects and realms solve different problems, and most deployments use both. Projects are organizational. You use them to group containers by function: frontend, backend, data-pipeline. Team members get permissions at the project level, and quotas are set per project.
Realms are security boundaries. You use them to isolate environments: production, staging, client-A, client-B. Auth tokens are restricted per realm, and API visibility is scoped per realm.
Combined, a deployment looks like this:
Realm: production├── Project: api-services│ ├── Container: auth-server│ ├── Container: user-api│ └── Container: payment-api├── Project: frontend│ ├── Container: web-app│ └── Container: admin-dashboard└── Project: infrastructure ├── Container: monitoring └── Container: log-aggregator
Realm: staging├── Project: api-services│ └── Container: staging-api (copy of prod)└── Project: frontend └── Container: staging-web (copy of prod)A CI token restricted to the staging realm can deploy as much as it needs to. It cannot see or modify production, and no API response it receives reveals that the production realm exists.
Realm and project consistency
Section titled “Realm and project consistency”When you create a container from a realm-scoped host, the API merges the subdomain realm into the container’s realm_ids. The parent project must already belong to that realm; otherwise the API rejects the create with 403: "Project is not in requested realm {realmId}". This prevents a realm-scoped container from ending up under a project outside that realm.
# Create a realm-restricted auth token for your CI pipelinehoody auth create \ --alias "ci-staging-deploy" \ --expires-at "2026-07-12T00:00:00Z" \ --realm-ids "60d5f1f3a3b4f9c3e8a1b2c3" \ --no-allow-no-realm
# This token can only operate on the staging realm# It cannot see production resources; the API filters them outimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
// Create a realm-restricted token for CIconst token = await client.api.authTokens.create({ alias: 'ci-staging-deploy', expires_at: '2026-07-12T00:00:00Z', realm_ids: ['60d5f1f3a3b4f9c3e8a1b2c3'], allow_no_realm: false});
// CI uses this token with the staging realm hostconst ciClient = new HoodyClient({ baseURL: 'https://60d5f1f3a3b4f9c3e8a1b2c3.api.hoody.com', token: token.data.token});
// This client can only see staging resourcesconst containers = await ciClient.api.containers.list();# Create a realm-restricted tokencurl -X POST "https://api.hoody.com/api/v1/auth/tokens" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "alias": "ci-staging-deploy", "expires_at": "2026-07-12T00:00:00Z", "realm_ids": ["60d5f1f3a3b4f9c3e8a1b2c3"], "allow_no_realm": false }'
# Use it on the realm-scoped hostcurl "https://60d5f1f3a3b4f9c3e8a1b2c3.api.hoody.com/api/v1/containers/" \ -H "Authorization: Bearer hdy_CiToken123..."One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Mint a realm-restricted CI token, then call the realm-scoped host with the token it returns in place of CI_TOKEN. That second link only ever sees the staging realm.
# Create CI token
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/auth/tokens&method=POST&bearer_token=TOKEN&json={"alias":"ci-staging-deploy","expires_at":"2026-07-12T00:00:00Z","realm_ids":["60d5f1f3a3b4f9c3e8a1b2c3"],"allow_no_realm":false}&response=transparent
# Use it on the realm host
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://60d5f1f3a3b4f9c3e8a1b2c3.api.hoody.com/api/v1/containers/&method=GET&bearer_token=CI_TOKEN&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
The bootstrap exception
Section titled “The bootstrap exception”There is one case where a realm-restricted token can call the unscoped base host: discovery. A client holding a freshly issued realm-restricted token does not yet know which realm host to use, so Hoody allows GET /api/v1/auth/tokens/me on https://api.hoody.com for any token. The response carries a restrictions object that tells the client:
restrictions.allowed_realm_ids: which realms this token can accessrestrictions.requires_realm_scope: whether a realm-scoped host is requiredrestrictions.active_realm_id: the currently active realm (if any)
SDK clients and automation tools use this to self-configure on startup: call /me, read the realm list, and switch to the matching host.
Common patterns
Section titled “Common patterns”One realm per environment
Section titled “One realm per environment”The most common layout. A production token cannot delete staging containers, even by accident, and a staging token cannot see production data.
Realm: production → Token: prod-deploy (expires: never, IP-locked)Realm: staging → Token: ci-staging (expires: 90d)Realm: development → Token: dev-team (expires: 30d)One realm per client
Section titled “One realm per client”For SaaS multi-tenancy, each client’s containers live in their own realm. Client-scoped tokens cannot see other clients’ infrastructure.
Realm: client-acme → Token: acme-api-keyRealm: client-globex → Token: globex-api-keyRealm: internal → Token: admin-full-accessOne realm per AI agent
Section titled “One realm per AI agent”Give each AI agent a realm-restricted token scoped to the containers it manages. The rest of your account is out of the agent’s reach: outside its realm, the token’s API contains nothing.
Realm: agent-deploy → Agent deploys to 3 containersRealm: agent-monitor → Agent reads metrics from 10 containersRealm: agent-test → Agent runs tests in isolated containersDelegated access for external parties
Section titled “Delegated access for external parties”Give a freelancer, auditor, or support engineer access to specific containers without exposing your entire account. Create a realm, assign the relevant containers, issue a restricted token with a short expiration and IP allowlist.
# Create a short-lived, IP-locked token for a freelancerhoody auth create \ --alias "freelancer-debug" \ --expires-at "2026-04-20T00:00:00Z" \ --ip-whitelist "203.0.113.44" \ --realm-ids "507f1f77bcf86cd799439011" \ --no-allow-no-realmconst token = await client.api.authTokens.create({ alias: 'freelancer-debug', expires_at: '2026-04-20T00:00:00Z', ip_whitelist: ['203.0.113.44'], realm_ids: ['507f1f77bcf86cd799439011'], allow_no_realm: false});
// Share token + realm URL with freelancer:// https://507f1f77bcf86cd799439011.api.hoody.comcurl -X POST "https://api.hoody.com/api/v1/auth/tokens" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "alias": "freelancer-debug", "expires_at": "2026-04-20T00:00:00Z", "ip_whitelist": ["203.0.113.44"], "realm_ids": ["507f1f77bcf86cd799439011"], "allow_no_realm": false }'One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Issue the short-lived, IP-locked token in one call. Share the resulting token plus the realm-scoped URL with the freelancer.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/auth/tokens&method=POST&bearer_token=TOKEN&json={"alias":"freelancer-debug","expires_at":"2026-04-20T00:00:00Z","ip_whitelist":["203.0.113.44"],"realm_ids":["507f1f77bcf86cd799439011"],"allow_no_realm":false}&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
When the work is done, disable or delete the token. Access ends immediately.
A platform built on Hoody
Section titled “A platform built on Hoody”Realms, auth tokens, and the SDK combine into a resale pattern: every customer you onboard gets their own isolated Hoody API, with containers, terminals, files, browsers, AI agents, cron, and databases, without you building any of it. You are the provider and Hoody is the infrastructure; your customers hold a token you issued rather than a Hoody account.
import { HoodyClient } from 'hoody-sdk';
// You are the platform provider: log in with account credentialsconst hoody = await HoodyClient.authenticate('https://api.hoody.com', { username: process.env.PROVIDER_EMAIL!, password: process.env.PROVIDER_PASSWORD!,});
// Pick a realm ID for the new customer (24-hex)const realmId = '507f1f77bcf86cd799439011';
// 1. Pre-create at least one project in the realm.// The external_customer template denies projects.create,// so the customer cannot create one themselves.const project = await hoody.api.projects.create({ alias: 'acme-workspace', realm_ids: [realmId],});
// 2. Optionally pre-create containers in that project / realm.// Anything you want the customer to see must carry their realm_id.await hoody.api.containers.create(project.data!.id, { server_id: process.env.SERVER_ID!, name: 'acme-box-1', hoody_kit: true, realm_ids: [realmId],});
// 3. Issue the customer a realm-scoped tokenconst created = await hoody.api.authTokens.create({ alias: 'Customer Acme Corp', permission_template: 'external_customer', realm_ids: [realmId], allow_no_realm: false, ip_whitelist: ['203.0.113.0/24'], expires_at: '2026-12-31T00:00:00Z',});
// The token value is returned once, at creation.// List and get endpoints never return it again; store it now.const customerToken = created.data!.token;
// Hand the customer: token + https://507f1f77bcf86cd799439011.api.hoody.com// Your customer, using the token you issuedconst acme = new HoodyClient({ baseURL: 'https://507f1f77bcf86cd799439011.api.hoody.com', token: 'hdy_tokenYouGaveThem...',});
// They see only their containers (the ones you assigned to their realm)const { data } = await acme.api.containers.list();
// Scope to a container, then run commands, read files, drive browsersconst box = await acme.withContainer(data.containers![0]!);await box.terminal.execution.execute({ command: 'deploy.sh' });const logs = await box.files.get('/var/log/deploy.log', { responseType: 'text' });
// They cannot see your other customers, your billing,// or anything outside their realm. The API filters those// resources out instead of returning "access denied".# Provider: log in first to get a JWT (an auth token can mint tokens only if it holds resources.create_tokens)JWT=$(curl -s -X POST "https://api.hoody.com/api/v1/users/auth/login" \ -H "Content-Type: application/json" \ -d "{\"email\": \"$PROVIDER_EMAIL\", \"password\": \"$PROVIDER_PASSWORD\"}" \ | jq -r .data.token)
# Provider: pre-create a realm-scoped project (external_customer cannot)curl -X POST "https://api.hoody.com/api/v1/projects/" \ -H "Authorization: Bearer $JWT" \ -H "Content-Type: application/json" \ -d '{"alias": "acme-workspace", "realm_ids": ["507f1f77bcf86cd799439011"]}'
# Provider: create the customer token (returns the secret ONCE)curl -X POST "https://api.hoody.com/api/v1/auth/tokens" \ -H "Authorization: Bearer $JWT" \ -H "Content-Type: application/json" \ -d '{ "alias": "Customer Acme Corp", "permission_template": "external_customer", "realm_ids": ["507f1f77bcf86cd799439011"], "allow_no_realm": false, "ip_whitelist": ["203.0.113.0/24"], "expires_at": "2026-12-31T00:00:00Z" }'
# Customer: use their scoped APIcurl "https://507f1f77bcf86cd799439011.api.hoody.com/api/v1/containers/" \ -H "Authorization: Bearer hdy_customerToken..."What each customer gets:
| Capability | How it works |
|---|---|
| Isolated containers | Realm filtering: only their resources exist |
| Terminal access | box.terminal.*: run commands, stream output |
| File management | box.files.*: CRUD, glob, grep, archives |
| Browser automation | box.browser.*: headless Chromium, screenshots |
| GUI app streaming | box.display.*: X11 display in a URL |
| Scheduled tasks | box.cron.*: crontab via REST |
| Database access | box.sqlite.*: SQL queries, key-value store |
| AI agent | box.agent.*: sessions, prompts, memory |
| Notifications | box.notifications.*: push to desktop/mobile |
What you control:
- Permissions:
external_customerblocks billing, AI, server management, andprojects.createby default (so you must pre-create projects in the realm). Usedev_team,read_only, or fully custom permissions for finer control. - IP allowlists: lock tokens to customer IP ranges
- Expiration: auto-revoke after a date
- Enable/disable: suspend access immediately without deleting the token
- Public profiles: attach metadata (company name, tier, display info) to tokens via
public_storage. Pair it with apublic_key(ED25519, 64 hex chars) so the profile is resolvable for third-party lookup; both can be set at creation or later viaPUT /api/v1/auth/tokens/me/public-profile(at least one of the two fields must be provided).
The pattern is the same at ten customers or ten thousand. Each customer gets a realm-scoped endpoint and a token, and you manage them all through the same SDK.
Recommended structure
Section titled “Recommended structure”- Use projects for application boundaries:
frontend,backend,ops,ml-pipeline. - Use realms for environment and tenant isolation:
production,staging, per-client realms. - Issue separate auth tokens per realm and per application; it makes auditing and revocation easier.
- Use the bootstrap endpoint (
GET /api/v1/auth/tokens/me) in SDK and automation startup flows to self-configure realm hosts.
Realm discovery
Section titled “Realm discovery”# List all realm IDs across your resourceshoody realms list// List realm IDs found across your resourcesconst realms = await client.api.realms.list();console.log(realms.data.realm_ids);// ['507f1f77bcf86cd799439011', '60d5f1f3a3b4f9c3e8a1b2c3', ...]# Deduplicated list of realm IDs from your resourcescurl "https://api.hoody.com/api/v1/realms/" \ -H "Authorization: Bearer $HOODY_TOKEN"One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Fetch the deduplicated list of realm IDs across your resources as one link.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/realms/&method=GET&bearer_token=TOKEN&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
Projects organize and realms isolate: a project groups containers for a team or application, and a realm decides which resources a token’s API contains. Together they cover multi-tenancy without a separate policy layer to maintain.