# Realms & Projects

**Page:** concepts/realms-projects

[Download Raw Markdown](./concepts/realms-projects.md)

---

# 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

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`, `delete` per 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.


  
    ```bash
    # Create a project
    hoody projects create --alias "production-api"

    # List your projects
    hoody projects list

    # Create a container inside the project
    hoody containers create --project $PROJECT_ID \
      --server-id $SERVER_ID \
      --name "api-server" \
      --hoody-kit
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Create a project
    const project = await client.api.projects.create({
      alias: 'production-api'
    });

    // List all projects
    const projects = await client.api.projects.list();

    // Create a container in that project
    const container = await client.api.containers.create(
      project.data.id,
      {
        server_id: SERVER_ID,
        name: 'api-server',
        hoody_kit: true
      }
    );
    ```
  
  
    ```bash
    # Create a project
    curl -X POST "https://api.hoody.icu/api/v1/projects" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"alias": "production-api"}'

    # List projects
    curl "https://api.hoody.icu/api/v1/projects" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # Create a container in the project
    curl -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
      }'
    ```
  



Think of projects like email folders -- create as many as you need. A project for each application, each team, each experiment. The overhead is zero.


---

## 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

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.icu
Realm-scoped: https://507f1f77bcf86cd799439011.api.hoody.icu
```

When you call a realm-scoped host:
- **Read operations** return only resources whose `realm_ids` includes that realm
- **Write operations** automatically merge the realm into `realm_ids` on 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.


Realms are NOT network isolation. They do not create firewalls or private networks between containers. Realms scope API visibility -- what your token can see and control. For network-level isolation, use [Container Network](/foundation/networking/network/) and [Firewall](/foundation/networking/firewall/) configuration.


### Realm-Scoped API Calls


  
    ```bash
    # List containers visible in a specific realm
    hoody --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 restrictions
    hoody auth get-current
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    // Realm-scoped client -- only sees resources in this realm
    const realmClient = new HoodyClient({
      baseURL: 'https://507f1f77bcf86cd799439011.api.hoody.icu',
      token: process.env.HOODY_TOKEN
    });

    // This only returns containers assigned to realm 507f1f77bcf86cd799439011
    const containers = await realmClient.api.containers.list();

    // Projects created here auto-inherit the realm
    const project = await realmClient.api.projects.create({
      alias: 'prod-services'
    });
    // project.data.realm_ids includes '507f1f77bcf86cd799439011'
    ```
  
  
    ```bash
    # List containers in a realm
    curl "https://507f1f77bcf86cd799439011.api.hoody.icu/api/v1/containers" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # Create a project scoped to a realm
    curl -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 restrictions
    curl "https://api.hoody.icu/api/v1/auth/tokens/me" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  


---

## 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

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.


  
    ```bash
    # Create a realm-restricted auth token for your CI pipeline
    hoody 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 world
    ```
  
  
    ```typescript
    import { 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 CI
    const 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 host
    const ciClient = new HoodyClient({
      baseURL: 'https://60d5f1f3a3b4f9c3e8a1b2c3.api.hoody.icu',
      token: token.token
    });

    // This client can only see staging resources
    const containers = await ciClient.api.containers.list();
    ```
  
  
    ```bash
    # Create a realm-restricted token
    curl -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 host
    curl "https://60d5f1f3a3b4f9c3e8a1b2c3.api.hoody.icu/api/v1/containers" \
      -H "Authorization: Bearer hdy_CiToken123..."
    ```
  


---

## 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 access
- `restrictions.requires_realm_scope` -- whether a realm-scoped host is required
- `restrictions.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.


This bootstrap endpoint is for token introspection only. All other API calls from a realm-restricted token must use a realm-scoped host (`https://{realmId}.api.hoody.icu`). Calling the unscoped host returns a `403: "This token requires a realm-scoped URL"`.


---

## Real-World Patterns

### 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

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-key
Realm: client-globex  →  Token: globex-api-key
Realm: internal       →  Token: admin-full-access
```

### 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 containers
Realm: agent-monitor  →  Agent reads metrics from 10 containers
Realm: agent-test     →  Agent runs tests in isolated containers
```

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


  
    ```bash
    # Create a short-lived, IP-locked token for a freelancer
    hoody auth create \
      --alias "freelancer-debug" \
      --expires-at "2026-04-20T00:00:00Z" \
      --ip-whitelist "203.0.113.44" \
      --realm-ids "507f1f77bcf86cd799439011" \
      --no-allow-no-realm
    ```
  
  
    ```typescript
    const 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.icu
    ```
  
  
    ```bash
    curl -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

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


**Auth tokens cannot create other auth tokens.** The `POST /api/v1/auth/tokens` endpoint returns `403` when called with an auth token (`hdy_...`). Use account credentials instead -- either `HoodyClient.authenticate(baseURL, { username, password })` or a JWT from `POST /api/v1/users/auth/login`. A JWT bearer is fine; only auth tokens are forbidden.



  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    // You: the platform provider — log in with account credentials
    const 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 token
    const 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
    ```
  
  
    ```typescript
    // Your customer -- using the token you issued
    const 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 browsers
    const 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.
    ```
  
  
    ```bash
    # 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 API
    curl "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_customer` blocks billing, AI, server management *and `projects.create`* by default (so you must pre-create projects in the realm). Use `dev_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 a `public_key` (ED25519, 64 hex chars) -- can be set at creation or later via `PUT /auth/tokens/me/public-profile`. `public_storage` without a `public_key` is 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.


Think of it this way: you are not giving customers access to *your* Hoody account. You are giving each customer their own Hoody API that happens to be backed by your infrastructure. Realms make this possible without any multi-tenancy code on your side.


---

## Recommended Structure

1. **Use projects for application boundaries** -- `frontend`, `backend`, `ops`, `ml-pipeline`
2. **Use realms for environment and tenant isolation** -- `production`, `staging`, per-client realms
3. **Issue separate auth tokens per realm and per application** -- easier auditing, easier revocation
4. **Use the bootstrap endpoint** (`GET /api/v1/auth/tokens/me`) in SDK and automation startup flows to self-configure realm hosts

---

## Discovering Your Realms


  
    ```bash
    # List all realm IDs across your resources
    hoody realms list
    ```
  
  
    ```typescript
    // List realm IDs found across your resources
    const realms = await client.api.realms.list();
    console.log(realms.data);
    // ['507f1f77bcf86cd799439011', '60d5f1f3a3b4f9c3e8a1b2c3', ...]
    ```
  
  
    ```bash
    # Deduplicated list of realm IDs from your resources
    curl "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.**