Skip to content

Commit 5560181

Browse files
khaliqgantclaude
andauthored
feat: idempotent workspace creation — same name+owner returns existing (#115)
* feat: idempotent workspace creation — same name+owner returns existing Same API key + same name returns existing workspace (200). Different API keys can share workspace names. Changes: - engine/workspace.ts — createWorkspace returns existing if same name+apiKeyHash - routes/workspace.ts — returns 200 for existing, 201 for new - sdk relay.ts — ensureWorkspace simplified (no 409 dependency) - local main.rs — matches server idempotent behavior - types/workspace.ts — CreateWorkspaceResult type - openapi.yaml — updated responses (200 added, 409 removed) - README.md — updated docs - tests updated Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add engine integration tests for idempotent workspace creation Tests with real SQLite DB (via FakeD1): - New workspace returns created: true - Same name + same API key returns existing (created: false) - Same name + different keys creates separate workspaces - ownerApiKeyHash lookup works - Anonymous creates always produce new workspaces - Conflicting key + hash throws - getWorkspaceByName handles duplicates Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * update openapi --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9f6a2c9 commit 5560181

11 files changed

Lines changed: 786 additions & 163 deletions

File tree

README.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,17 @@ npx tsx quickstart.ts
8888

8989
That is the canonical onboarding loop: create workspace, register agents, connect realtime streams, and watch messages flow live.
9090

91-
If you want idempotent setup by workspace name, use `ensureWorkspace()`:
91+
Workspace names are not globally unique. Workspace creation is idempotent for the same workspace name and API key: repeating that combination returns the existing workspace instead of creating another one.
92+
93+
If you want an explicit SDK helper that tells you whether setup returned an existing workspace or created a new one, use `ensureWorkspace()`:
9294

9395
```ts
94-
const ensured = await RelayCast.ensureWorkspace('my-project');
96+
const ensured = await RelayCast.ensureWorkspace('my-project', {
97+
apiKey: knownWorkspaceKey,
98+
});
9599

96100
if (ensured.existed) {
97-
console.log(`Workspace already exists as ${ensured.id}`);
101+
console.log(`Workspace already exists as ${ensured.workspaceId}`);
98102
// Existing workspace keys are not recoverable from the API.
99103
// Reuse the known rk_live_* key you already have for this workspace.
100104
} else {
@@ -248,7 +252,9 @@ Remote Streamable HTTP config:
248252
## REST Quick Start
249253

250254
```bash
251-
# Create workspace (names must be unique)
255+
# Create workspace
256+
# Workspace names are not globally unique.
257+
# Reusing the same name with the same Authorization bearer workspace key returns the existing workspace.
252258
curl -X POST https://api.relaycast.dev/v1/workspaces \
253259
-H "Content-Type: application/json" \
254260
-d '{"name": "my-project"}'

openapi.yaml

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,22 @@ components:
5050
type: string
5151
format: date-time
5252

53+
CreateWorkspaceResponse:
54+
type: object
55+
required:
56+
- workspace_id
57+
- created_at
58+
properties:
59+
workspace_id:
60+
type: string
61+
description: Snowflake ID of the workspace
62+
api_key:
63+
type: string
64+
description: Workspace API key (only returned on first creation)
65+
created_at:
66+
type: string
67+
format: date-time
68+
5369
WorkspaceLookup:
5470
type: object
5571
required:
@@ -345,9 +361,12 @@ paths:
345361
/workspaces:
346362
post:
347363
summary: Create workspace
348-
description: Create a new workspace and get an API key. Workspace names must be unique.
364+
description: Create a workspace and get an API key when a new workspace is created. This operation is idempotent by workspace name for the same bearer workspace key: if you repeat the call with the same name and the same `Authorization: Bearer rk_*` key, the existing workspace is returned with a `200` response; otherwise a new workspace is created and returned with a `201` response.
349365
tags:
350366
- Workspaces
367+
security:
368+
- {}
369+
- workspaceKey: []
351370
requestBody:
352371
required: true
353372
content:
@@ -360,6 +379,17 @@ paths:
360379
name:
361380
type: string
362381
responses:
382+
'200':
383+
description: Existing workspace returned (idempotent)
384+
content:
385+
application/json:
386+
schema:
387+
type: object
388+
properties:
389+
ok:
390+
type: boolean
391+
data:
392+
$ref: '#/components/schemas/CreateWorkspaceResponse'
363393
'201':
364394
description: Workspace created
365395
content:
@@ -370,19 +400,13 @@ paths:
370400
ok:
371401
type: boolean
372402
data:
373-
$ref: '#/components/schemas/Workspace'
403+
$ref: '#/components/schemas/CreateWorkspaceResponse'
374404
'400':
375405
description: Invalid request
376406
content:
377407
application/json:
378408
schema:
379409
$ref: '#/components/schemas/ErrorResponse'
380-
'409':
381-
description: Workspace name already exists
382-
content:
383-
application/json:
384-
schema:
385-
$ref: '#/components/schemas/ErrorResponse'
386410

387411
/workspaces/by-name/{name}:
388412
get:

0 commit comments

Comments
 (0)