Realms & Projects
Section titled “Realms & Projects”Every other platform treats multi-tenancy as an afterthought. IAM roles. Org charts. Nested permission hierarchies that take a PhD to debug. You end up with a CI token that can nuke production because someone forgot to scope it.
Hoody built isolation into the architecture from day one. Two primitives. Two problems solved.
Projects organize your resources — containers, quotas, team permissions. Think of them as folders for your computers.
Realms isolate API visibility. A realm-scoped token physically cannot see resources outside its realm. Not “permission denied.” Not “unauthorized.” The resources do not exist in its universe.
Together, they give you organizational clarity (projects) and security isolation (realms) without a 47-page IAM policy document.
Projects: Organize Your Computers
Section titled “Projects: Organize Your Computers”A project is a boundary for containers. Every container belongs to exactly one project. Projects give you:
- Container grouping —
frontend,backend,ml-pipeline,staging - Team permissions —
read,edit,deleteper member - Quotas — resource limits, prespawn settings
- Billing — costs tracked per project
When you create a container, you create it inside a project. When you list containers, you can filter by project. When you share access with a teammate, you share at the project level.
# 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-ai/hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', 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.icu/api/v1/projects" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"alias": "production-api"}'
# List projectscurl "https://api.hoody.icu/api/v1/projects" \ -H "Authorization: Bearer $HOODY_TOKEN"
# Create a container in the projectcurl -X POST "https://api.hoody.icu/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 }'Realms: Invisible Walls in the API
Section titled “Realms: Invisible Walls in the API”Here is the question that every multi-tenant system eventually faces: how do you stop a token from seeing things it should not see?
Traditional platforms add permission checks. The resource exists, the token just cannot access it. That means a misconfigured permission can expose everything. One overly permissive role, one leaked token, and the blast radius is your entire account.
Realms take a fundamentally different approach. A realm does not restrict access to resources. It removes resources from existence.
How Realms Work
Section titled “How Realms Work”A realm is a 24-hex identifier (e.g., 507f1f77bcf86cd799439011) that acts as an API isolation scope. Resources have 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.icuRealm-scoped: https://507f1f77bcf86cd799439011.api.hoody.icuWhen 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 your token is realm-restricted and you call the unscoped host, the API rejects it. The token literally cannot operate without a realm scope.
Realm-Scoped API Calls
Section titled “Realm-Scoped API Calls”# List containers visible in a specific realmhoody --base-url "https://507f1f77bcf86cd799439011.api.hoody.icu" \ containers list
# Create a project in a realm (auto-assigned realm_ids)hoody --base-url "https://507f1f77bcf86cd799439011.api.hoody.icu" \ projects create --alias "prod-services"
# Discover your token's realm restrictionshoody auth get-currentimport { HoodyClient } from '@hoody-ai/hoody-sdk';
// Realm-scoped client -- only sees resources in this realmconst realmClient = new HoodyClient({ baseURL: 'https://507f1f77bcf86cd799439011.api.hoody.icu', 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.icu/api/v1/containers" \ -H "Authorization: Bearer $HOODY_TOKEN"
# Create a project scoped to a realmcurl -X POST "https://507f1f77bcf86cd799439011.api.hoody.icu/api/v1/projects" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"alias": "prod-services"}'
# Discover token realm restrictionscurl "https://api.hoody.icu/api/v1/auth/tokens/me" \ -H "Authorization: Bearer $HOODY_TOKEN"How They Work Together
Section titled “How They Work Together”Projects and realms solve different problems, and they layer perfectly.
Projects are organizational. You use them to group containers by function — frontend, backend, data-pipeline. Team members get permissions at the project level. 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. API visibility is scoped per realm.
The combination is powerful:
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 all day long. It cannot see, touch, or even know that the production realm exists.
Realm/Project Consistency
Section titled “Realm/Project Consistency”When you create a container from a realm-scoped host, the parent project must already belong to that realm. Otherwise Hoody rejects the request with a 403.
This prevents a subtle but dangerous inconsistency: creating a realm-scoped container under an out-of-scope project.
# 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 -- they do not exist in its worldimport { HoodyClient } from '@hoody-ai/hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', 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.icu', token: token.token});
// This client can only see staging resourcesconst containers = await ciClient.api.containers.list();# Create a realm-restricted tokencurl -X POST "https://api.hoody.icu/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.icu/api/v1/containers" \ -H "Authorization: Bearer hdy_CiToken123..."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 freshly issued realm-restricted token does not know which realm host to use. So Hoody allows GET /api/v1/auth/tokens/me on https://api.hoody.icu 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, learn your realms, switch to the correct host.
Real-World Patterns
Section titled “Real-World Patterns”One Realm Per Environment
Section titled “One Realm Per Environment”The most common pattern. Production token cannot accidentally delete staging containers. 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”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”The safest approach for automation. Each AI agent gets a realm-restricted token scoped to only the containers it manages. A misbehaving agent cannot affect containers outside its realm.
Realm: agent-deploy → Agent deploys to 3 containersRealm: agent-monitor → Agent reads metrics from 10 containersRealm: agent-test → Agent runs tests in isolated containersDelegating Access to External Parties
Section titled “Delegating Access to 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.icucurl -X POST "https://api.hoody.icu/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 }'When the work is done, disable or delete the token. Access vanishes instantly.
Build Your Platform on Hoody
Section titled “Build Your Platform on Hoody”This is the pattern that changes what you can ship. Combine realms, auth tokens, and the full SDK, and every customer you onboard gets their own isolated Hoody API — containers, terminals, files, browsers, AI agents, cron, databases — without you building any of it.
You are the provider. Hoody is the infrastructure. Your customers never know.
import { HoodyClient } from '@hoody-ai/hoody-sdk';
// You: the platform provider — log in with account credentialsconst hoody = await HoodyClient.authenticate('https://api.hoody.icu', { 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 & get endpoints never return it again — store it now.const customerToken = created.data!.token;
// Hand the customer: token + https://507f1f77bcf86cd799439011.api.hoody.icu// Your customer -- using the token you issuedconst acme = new HoodyClient({ baseURL: 'https://507f1f77bcf86cd799439011.api.hoody.icu', token: 'hdy_tokenYouGaveThem...',});
// They see only their containers (the ones you assigned to their realm)const { data } = await acme.api.containers.list();const containerId = data.containers![0]!.id;
// Scope to a container, then run commands, read files, drive browsersconst box = await acme.withContainer(containerId);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. Not "access denied" --// those resources do not exist in their API.# Provider: log in first to get a JWT (auth tokens cannot mint tokens)JWT=$(curl -s -X POST "https://api.hoody.icu/api/v1/users/auth/login" \ -H "Content-Type: application/json" \ -d "{\"username\": \"$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.icu/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.icu/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.icu/api/v1/containers" \ -H "Authorization: Bearer hdy_customerToken..."What each customer gets (without you building it):
| 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 instantly without deleting the token
- Public profiles — attach metadata (company name, tier, display info) to tokens via
public_storage. Requires apublic_key(ED25519, 64 hex chars) — can be set at creation or later viaPUT /auth/tokens/me/public-profile.public_storagewithout apublic_keyis rejected.
Scale to ten customers or ten thousand. Each gets their own realm-scoped endpoint, their own token, their own universe. You manage it 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 — easier auditing, easier revocation
- Use the bootstrap endpoint (
GET /api/v1/auth/tokens/me) in SDK and automation startup flows to self-configure realm hosts
Discovering Your Realms
Section titled “Discovering Your Realms”# 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);// ['507f1f77bcf86cd799439011', '60d5f1f3a3b4f9c3e8a1b2c3', ...]# Deduplicated list of realm IDs from your resourcescurl "https://api.hoody.icu/api/v1/realms" \ -H "Authorization: Bearer $HOODY_TOKEN"Projects organize. Realms isolate. One gives you clarity. The other gives you walls. Together, they give you multi-tenancy that does not require a 47-page IAM policy to understand.