---
name: rtie-auth-and-tenancy
description: Understand RTIE's two-layer auth model, actor types, API key scopes, workspace_type gating, and how every request resolves to a tenant.
version: "0.1.0"
---

# RTIE Auth and Tenancy

RTIE has a **two-layer auth model**. Every request resolves to a `(tenantId, actorId, actorType)` triple before any module code runs.

## Layer 1 — Credential types

### RTIE API Key
- Format: `rtie_live_*` (production) or `rtie_test_*` (development)
- Passed as: `Authorization: Bearer <key>` or `X-API-Key: <key>`
- Scope: one tenant, one workspace_type, one plan
- Used by: backend servers, agents, CI pipelines, and the RTIE MCP server

### Supabase JWT
- Issued by Supabase Auth after email/password, OAuth, or magic link
- Passed as: `Authorization: Bearer <jwt>`
- Used by: console users (B2B), end users (buyers, bidders, gamers, learners)
- RTIE maps the Supabase `sub` to an internal `users.id` UUID

```typescript
// Both resolve through the same middleware
import { resolveIdentity } from "@rtie/middleware/hono";

app.use("*", resolveIdentity);

// Inside a handler:
const { tenantId, actorId, actorType } = c.var.identity;
// actorType: "api_key" | "console_user" | "end_user" | "agent"
```

## Layer 2 — Internal identity model

**Never use the auth provider's user ID as a primary key.**

```sql
-- Correct: RTIE internal UUID with external_auth_id as a unique indexed lookup
users (
  id               UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  external_auth_id TEXT UNIQUE,   -- Supabase Auth user ID (sub)
  email            TEXT,
  created_at       TIMESTAMPTZ
)
```

## Tenant membership and roles

```sql
-- Assigns a user to a tenant with a role
tenant_members (
  user_id   UUID REFERENCES users(id),
  tenant_id UUID REFERENCES tenants(id),
  role      TEXT  -- owner | admin | developer | viewer
)

-- Pending invitations (accepted via secure token in email)
tenant_invites (
  tenant_id     UUID,
  invited_email TEXT,
  role          TEXT,
  token         TEXT UNIQUE,    -- secure random, expires 7 days
  accepted_at   TIMESTAMP
)
```

## Tenant archetypes (workspace_type)

The `workspace_type` field on a tenant determines which modules and MCP tools are visible:

| workspace_type | Description | Example |
|---|---|---|
| `internal` | RTIE Labs first-party | RTIE Labs platform staff |
| `vertical` | RTIE Labs product tenant | vAuction, vArcade tenant records |
| `operator` | Third-party operator running a product | BidOnItLive |
| `developer` | Third-party building on RTIE Core | An indie dev building a game |

**Platform tools** (agent-work, memory, work) are only visible to:
- `workspace_type === "internal"`, or
- `plan === "enterprise"`

**Product-bundle tools** (auctions, arcade, etc.) are only visible to:
- `workspace_type` in `["operator", "vertical", "internal"]`

Developer workspaces see: rooms, chat, presence, sync, webhooks, actions, payments.

## API key scopes

When creating API keys in the console, you set the key scope:

```
read     — GET requests only
write    — GET + POST/PATCH/PUT
admin    — all methods including DELETE + module management
```

The MCP server honors key scope — `auction_place_bid` requires `write` scope.

## Auth in Hono Workers

```typescript
import { resolveIdentity, requireRole } from "@rtie/middleware/hono";

// All routes: resolve identity
app.use("*", resolveIdentity);

// Require a minimum role for specific routes
app.post("/v1/my-module/admin-action",
  requireRole("admin"),
  async (c) => { ... }
);
```

## End-user auth (B2C buyers, bidders)

RTIE supports two patterns for end-user identity:

**Hosted auth (first-party products):** vAuction, vArcade, and other RTIE Labs apps use Supabase Auth (email/password, magic link, OAuth) and pass the JWT directly to RTIE API calls:

```typescript
const { data: { session } } = await supabase.auth.getSession();
const res = await fetch("https://api.rtie.ai/v1/rooms", {
  headers: { Authorization: `Bearer ${session?.access_token}` },
});
```

**BYOA (third-party runtime users):** Third-party operators can keep their existing identity provider (Auth0, Firebase, Cognito, custom OIDC, or guest identity). The app backend verifies the provider token, maps it to an RTIE actor, and mints a short-lived **RTIE session token** via `/v1/runtime/connect`:

```typescript
// Backend: exchange your provider token for an RTIE session token
const { session } = await fetch("https://api.rtie.ai/v1/runtime/connect", {
  method: "POST",
  headers: { "X-API-Key": process.env.RTIE_API_KEY!, "Content-Type": "application/json" },
  body: JSON.stringify({ actor: { type: "end_user", externalId: userId } }),
}).then(r => r.json());

// Frontend: use the RTIE session token for module API calls
const res = await fetch("https://api.rtie.ai/v1/rooms", {
  headers: { Authorization: `Bearer ${session.token}` },
});
```

RTIE validates the credential, maps it to an internal `users.id` UUID, and scopes the request to the tenant derived from the `Host` header or `X-Tenant-Id`.

## Multi-tenant isolation

All queries are automatically scoped. You never need to filter by tenant manually if you use `getRequestDb()`:

```typescript
// getRequestDb returns a db scoped to the current tenant's RLS context
const db = await getRequestDb(c);
// → queries automatically scoped to tenantId from auth
```

For direct drizzle queries, always filter:

```typescript
const tenantId = getTenantId(c);
await db.query.things.findMany({ where: (t, { eq }) => eq(t.tenantId, tenantId) });
```

Full docs: https://docs.rtie.ai/build/auth
