Skip to content

API Documentation

Matthew Smith edited this page Dec 3, 2025 · 3 revisions

Internal APIs (Convex)

All Convex APIs are accessed through the generated api object from convex/_generated/api.ts. They use Convex's real-time sync protocol and are type-safe.

Story Management

listStories (Query)

List all stories visible to the current user (public stories + user's own stories).

Type: Query (Read-only)
Access: Public
Endpoint: api.ui.listStories

Parameters:

{
  q?: string  // Optional search query to filter stories by title or summary
}

Example Usage:

const stories = useQuery(api.ui.listStories, { q: "adventure" });

Example Response:

[
  {
    "_id": "jd7x8k2p9m4n5q6r7s8t9u0v",
    "_creationTime": 1234567890000,
    "title": "The Dark Forest",
    "summary": "A mysterious adventure through an enchanted forest",
    "createdBy": "user123",
    "public": true,
    "rootNodeId": "node456",
    "tags": ["fantasy", "adventure"],
    "status": "published"
  }
]

Error Handling:

  • Throws "Unauthorized" if user is not authenticated
  • Throws "User not found" if user record doesn't exist
  • Returns empty array if no stories match the query

createStory (Mutation)

Create a new story with an initial root scene.

Type: Mutation (Write)
Access: Public (Authenticated)
Endpoint: api.ui.createStory

Parameters:

{
  title: string,              // Story title (required)
  summary?: string,           // Optional story description
  rootContent: string,        // Content for the opening scene (required)
  isPublic: boolean,          // Visibility setting
  rootNodeTitle?: string      // Optional title for opening scene
}

Example Usage:

const storyId = await createStory({
  title: "Mystery at Midnight",
  summary: "A thrilling detective story",
  rootContent: "It was a dark and stormy night...",
  isPublic: true,
  rootNodeTitle: "The Beginning"
});

Example Response:

"jd7x8k2p9m4n5q6r7s8t9u0v"  // Story ID

Error Handling:

  • Throws "Unauthorized" if user is not authenticated
  • Throws "No user record" if user doesn't exist in database
  • Validates that title and rootContent are non-empty strings

getStoryGraph (Query)

Retrieve complete story structure including all nodes and edges.

Type: Query (Read-only)
Access: Public
Endpoint: api.ui.getStoryGraph

Parameters:

{
  storyId: Id<"stories">  // Story ID to retrieve
}

Example Usage:

const graph = useQuery(api.ui.getStoryGraph, { storyId: story._id });

Example Response:

{
  "storyId": "jd7x8k2p9m4n5q6r7s8t9u0v",
  "rootNodeId": "node123",
  "nodes": [
    {
      "_id": "node123",
      "_creationTime": 1234567890000,
      "storyId": "jd7x8k2p9m4n5q6r7s8t9u0v",
      "role": "narrator",
      "title": "Opening Scene",
      "content": "The adventure begins...",
      "imageStorageId": "storage456",
      "metadata": {},
      "version": 1,
      "createdBy": "user123"
    }
  ],
  "edges": [
    {
      "_id": "edge456",
      "_creationTime": 1234567890000,
      "storyId": "jd7x8k2p9m4n5q6r7s8t9u0v",
      "fromNodeId": "node123",
      "toNodeId": "node789",
      "label": "Enter the forest",
      "conditions": null,
      "effects": null,
      "order": 0
    }
  ]
}

Error Handling:

  • Throws "Story not found" if storyId doesn't exist
  • Returns null if user doesn't have access to the story

Scene Management

updateNodeContent (Mutation)

Update the content of an existing scene.

Type: Mutation (Write)
Access: Public (Authenticated)
Endpoint: api.ui.updateNodeContent

Parameters:

{
  nodeId: Id<"nodes">,  // Scene ID to update
  content: string        // New content for the scene
}

Example Usage:

await updateNodeContent({
  nodeId: "node123",
  content: "The revised story content..."
});

Example Response:

null  // Success returns null

Error Handling:

  • Throws error if nodeId doesn't exist
  • Validates that content is a string

updateNodeTitle (Mutation)

Update the title of an existing scene.

Type: Mutation (Write)
Access: Public (Authenticated)
Endpoint: api.ui.updateNodeTitle

Parameters:

{
  nodeId: Id<"nodes">,  // Scene ID to update
  title: string          // New title for the scene
}

Example Usage:

await updateNodeTitle({
  nodeId: "node123",
  title: "The Dark Cave"
});

Example Response:

null  // Success returns null

Error Handling:

  • Throws error if nodeId doesn't exist
  • Validates that title is a string

createNodeAndEdge (Mutation)

Create a new scene and automatically connect it to an existing scene.

Type: Mutation (Write)
Access: Public (Authenticated)
Endpoint: api.ui.createNodeAndEdge

Parameters:

{
  storyId: Id<"stories">,    // Story to add the scene to
  fromNodeId: Id<"nodes">,   // Source scene to connect from
  label: string,             // Label for the choice/path
  content: string,           // Content for the new scene
  title?: string             // Optional title for the new scene
}

Example Usage:

const newNodeId = await createNodeAndEdge({
  storyId: "story123",
  fromNodeId: "node456",
  label: "Enter the cave",
  content: "The cave is dark and mysterious...",
  title: "Inside the Cave"
});

Example Response:

"node789"  // ID of newly created node

Error Handling:

  • Throws "fromNode not in story" if fromNodeId doesn't belong to the story
  • Throws error if storyId doesn't exist
  • Validates all required fields are present

createEdge (Mutation)

Create a connection between two existing scenes.

Type: Mutation (Write)
Access: Public (Authenticated)
Endpoint: api.ui.createEdge

Parameters:

{
  storyId: Id<"stories">,    // Story containing the scenes
  fromNodeId: Id<"nodes">,   // Source scene
  toNodeId: Id<"nodes">,     // Destination scene
  label: string              // Label for the choice/path
}

Example Usage:

await createEdge({
  storyId: "story123",
  fromNodeId: "node456",
  toNodeId: "node789",
  label: "Take the left path"
});

Example Response:

null  // Success returns null

Error Handling:

  • Throws "nodes not in story" if either node doesn't belong to the story
  • Validates that both nodes exist
  • Validates label is non-empty string

deleteEdge (Mutation)

Remove a connection between scenes.

Type: Mutation (Write)
Access: Public (Authenticated)
Endpoint: api.ui.deleteEdge

Parameters:

{
  edgeId: Id<"edges">  // Edge/path to delete
}

Example Usage:

await deleteEdge({ edgeId: "edge123" });

Example Response:

null  // Success returns null

Error Handling:

  • Throws error if edgeId doesn't exist
  • No validation on whether edge is in use

Image Management

generateUploadUrl (Mutation)

Generate a secure upload URL for an image file.

Type: Mutation (Write)
Access: Public (Authenticated)
Endpoint: api.image.generateUploadUrl

Parameters:

{}  // No parameters required

Example Usage:

const uploadUrl = await generateUploadUrl();

Example Response:

"https://convex-cloud-storage.s3.amazonaws.com/upload/..."  // Secure upload URL

Error Handling:

  • Returns a signed URL that expires after a short time
  • URL is single-use only

attachImageToNode (Mutation)

Associate an uploaded image with a scene node.

Type: Mutation (Write)
Access: Public (Authenticated)
Endpoint: api.image.attachImageToNode

Parameters:

{
  nodeId: Id<"nodes">,       // Scene to attach image to
  storageId: Id<"_storage">  // Storage ID from upload
}

Example Usage:

await attachImageToNode({
  nodeId: "node123",
  storageId: "storage456"
});

Example Response:

null  // Success returns null

Error Handling:

  • Throws "Node not found" if nodeId doesn't exist
  • Throws "File not found" if storageId doesn't exist
  • Throws "Image must be smaller than 10MB" if file exceeds size limit
  • Throws "File must be an image" if file is not an image type
  • Automatically deletes old image if node already has one

removeImageFromNode (Mutation)

Remove image from a scene node.

Type: Mutation (Write)
Access: Public (Authenticated)
Endpoint: api.image.removeImageFromNode

Parameters:

{
  nodeId: Id<"nodes">  // Scene to remove image from
}

Example Usage:

await removeImageFromNode({ nodeId: "node123" });

Example Response:

null  // Success returns null

Error Handling:

  • Throws "Node not found" if nodeId doesn't exist
  • Deletes image from storage if it exists
  • Removes reference from node

getNodeImageUrl (Query)

Get the signed URL for a scene's image.

Type: Query (Read-only)
Access: Public
Endpoint: api.image.getNodeImageUrl

Parameters:

{
  nodeId: Id<"nodes">  // Scene to get image URL for
}

Example Usage:

const imageUrl = useQuery(api.image.getNodeImageUrl, { nodeId: "node123" });

Example Response:

"https://convex-cloud-storage.s3.amazonaws.com/..."  // Signed image URL

Error Handling:

  • Returns null if node doesn't exist
  • Returns null if node has no image
  • Signed URL expires after a period of time

getNodeImageMetadata (Query)

Get metadata for a scene's image.

Type: Query (Read-only)
Access: Public
Endpoint: api.image.getNodeImageMetadata

Parameters:

{
  nodeId: Id<"nodes">  // Scene to get image metadata for
}

Example Usage:

const metadata = useQuery(api.image.getNodeImageMetadata, { nodeId: "node123" });

Example Response:

{
  "_id": "storage456",
  "_creationTime": 1234567890000,
  "contentType": "image/jpeg",
  "sha256": "abc123...",
  "size": 524288
}

Error Handling:

  • Returns null if node doesn't exist
  • Returns null if node has no image

Session Management

startSessionForMe (Mutation)

Create a new play session for the current user on a story.

Type: Mutation (Write)
Access: Public (Authenticated)
Endpoint: api.ui.startSessionForMe

Parameters:

{
  storyId: Id<"stories">  // Story to start a session for
}

Example Usage:

const sessionId = await startSessionForMe({ storyId: "story123" });

Example Response:

"session456"  // ID of newly created session

Error Handling:

  • Throws "Unauthorized" if user is not authenticated
  • Throws "Story not found" if storyId doesn't exist
  • Throws "Story not seeded" if story has no rootNodeId

listMySessions (Query)

Get all play sessions for the current user.

Type: Query (Read-only)
Access: Public (Authenticated)
Endpoint: api.ui.listMySessions

Parameters:

{}  // No parameters required

Example Usage:

const sessions = useQuery(api.ui.listMySessions, {});

Example Response:

[
  {
    "_id": "session123",
    "_creationTime": 1234567890000,
    "storyId": "story456",
    "createdBy": "user789",
    "title": "The Dark Forest",
    "participants": ["user789"],
    "currentNodeId": "node321",
    "flags": {},
    "score": 0,
    "isGroup": false,
    "storyTitle": "The Dark Forest"
  }
]

Error Handling:

  • Returns empty array if user is not authenticated
  • Returns empty array if user has no sessions

getSessionState (Query)

Get complete state of a play session including messages and available choices.

Type: Query (Read-only)
Access: Public (Authenticated)
Endpoint: api.ui.getSessionState

Parameters:

{
  sessionId: Id<"sessions">  // Session to retrieve state for
}

Example Usage:

const state = useQuery(api.ui.getSessionState, { sessionId: session._id });

Example Response:

{
  "session": {
    "_id": "session123",
    "storyId": "story456",
    "currentNodeId": "node789",
    "participants": ["user123"]
  },
  "messages": [
    {
      "_id": "msg123",
      "_creationTime": 1234567890000,
      "sessionId": "session123",
      "nodeId": "node789",
      "role": "narrator",
      "content": "You enter the dark forest...",
      "author": "narrator"
    }
  ],
  "choices": [
    {
      "_id": "edge456",
      "fromNodeId": "node789",
      "toNodeId": "node321",
      "label": "Follow the path",
      "order": 0
    }
  ]
}

Error Handling:

  • Throws "Unauthorized" if user is not authenticated
  • Throws "No access" if user is not a participant in the session
  • Returns null if sessionId doesn't exist

chooseEdge (Mutation)

Make a choice in a play session and progress the story.

Type: Mutation (Write)
Access: Public (Authenticated)
Endpoint: api.ui.chooseEdge

Parameters:

{
  sessionId: Id<"sessions">,  // Current session
  edgeId: Id<"edges">         // Choice to make
}

Example Usage:

await chooseEdge({
  sessionId: "session123",
  edgeId: "edge456"
});

Example Response:

null  // Success returns null

Error Handling:

  • Throws "Unauthorized" if user is not authenticated
  • Throws "No access" if user is not a participant
  • Throws "Edge invalid" if edge doesn't belong to the story

advanceSession (Mutation)

Automatically advance the session by choosing the first available edge.

Type: Mutation (Write)
Access: Public (Authenticated)
Endpoint: api.ui.advanceSession

Parameters:

{
  sessionId: Id<"sessions">  // Session to advance
}

Example Usage:

const edgeId = await advanceSession({ sessionId: "session123" });

Example Response:

"edge456"  // ID of chosen edge, or null if no choices available

Error Handling:

  • Throws "Unauthorized" if user is not authenticated
  • Throws "No access" if user is not a participant
  • Returns null if no choices are available

AI Assistant

suggestImprovements (Action)

Get AI-generated suggestions for improving scene content.

Type: Action (External API call)
Access: Public (Authenticated)
Endpoint: api.ai.suggestImprovements

Parameters:

{
  content: string,              // Scene content to analyze
  selectedAspects?: string      // Optional: specific aspects to focus on
}

Example Usage:

const result = await suggestImprovements({
  content: "The hero walked through the forest.",
  selectedAspects: "pacing and rhythm, sensory details"
});

Example Response:

{
  "aspects": "pacing and rhythm, sensory details, dialogue and voice",
  "suggestions": "1. Vary sentence length to improve pacing...\n2. Add sensory details...\n3. Include internal dialogue...",
  "exampleEdits": {
    "sceneTitle": "Through the Whispering Woods",
    "revisedText": "The hero's boots crunched against fallen leaves as she navigated the shadowy forest. A distant owl hooted, sending shivers down her spine.",
    "analysis": "This revision improves pacing by varying sentence structure and adds sensory details (sounds, textures, visual elements) to create a more immersive experience."
  }
}

Error Handling:

  • Throws "OPENAI_API_KEY not found" if API key is not configured
  • Throws "OpenAI API error" with status code if API request fails
  • Returns error message if response parsing fails

rewriteContent (Action)

Rewrite scene content with optional tone or feedback modifications.

Type: Action (External API call)
Access: Public (Authenticated)
Endpoint: api.ai.rewriteContent

Parameters:

{
  content: string,      // Content to rewrite
  tone?: string,        // Optional: desired tone (e.g., "horror", "comedic")
  feedback?: string     // Optional: specific feedback to incorporate
}

Example Usage:

const rewritten = await rewriteContent({
  content: "The hero walked through the forest.",
  tone: "horror"
});

Example Response:

"The hero stumbled through the suffocating darkness of the forest, where every shadow seemed to writhe with malevolent intent. Gnarled branches reached out like skeletal fingers, scraping against flesh as whispers echoed from unseen depths."

Error Handling:

  • Throws "OPENAI_API_KEY not found" if API key is not configured
  • Throws "OpenAI API request failed" with details if API call fails
  • Throws "Unexpected response structure" if response format is invalid

enhanceContent (Action)

Expand and enhance scene content with more detail and depth.

Type: Action (External API call)
Access: Public (Authenticated)
Endpoint: api.ai.enhanceContent

Parameters:

{
  content: string,           // Content to enhance
  targetLength?: string      // Optional: target length (e.g., "2-3 paragraphs")
}

Example Usage:

const enhanced = await enhanceContent({
  content: "The hero entered the castle.",
  targetLength: "2-3"
});

Example Response:

"The hero approached the towering castle gates, her heart pounding with anticipation. Ancient stone walls loomed overhead, weathered by centuries of storms and sieges. As she pushed through the heavy wooden doors, their rusted hinges groaning in protest, the musty scent of age-old tapestries and forgotten memories filled her nostrils.\n\nInside, the grand entrance hall stretched before her like a cavern of shadows and secrets. Dust motes danced in the pale shafts of light filtering through high windows, illuminating faded portraits of long-dead nobles who seemed to watch her every move with knowing eyes."

Error Handling:

  • Throws "OPENAI_API_KEY not found" if API key is not configured
  • Throws "OpenAI API request failed" with details if API call fails
  • Throws "Unexpected response structure" if response format is invalid

generateChoices (Action)

Generate AI-powered story branching choices from current content.

Type: Action (External API call)
Access: Public (Authenticated)
Endpoint: api.ai.generateChoices

Parameters:

{
  content: string,        // Current scene content
  numChoices?: number     // Optional: number of choices to generate (default: 3)
}

Example Usage:

const result = await generateChoices({
  content: "You stand at a crossroads in the forest.",
  numChoices: 3
});

Example Response:

{
  "choices": [
    {
      "label": "Take the sunlit path to the east",
      "title": "The Eastern Trail",
      "description": "Warm sunlight filters through the canopy as you follow the well-worn eastern path. Birds chirp overhead, and the scent of wildflowers fills the air."
    },
    {
      "label": "Venture into the misty western woods",
      "title": "Into the Mist",
      "description": "A thick fog obscures your vision as you step onto the overgrown western trail. The air grows cold and damp, and strange sounds echo from the depths."
    },
    {
      "label": "Investigate the abandoned cottage to the north",
      "title": "The Forgotten Home",
      "description": "A small, dilapidated cottage comes into view through the trees. Its windows are dark and empty, but smoke rises from the chimney..."
    }
  ]
}

Error Handling:

  • Throws "OPENAI_API_KEY not found" if API key is not configured
  • Throws "OpenAI API request failed" with details if API call fails
  • Throws "Invalid response format" if choices array is missing or invalid

Saved Suggestions

saveSuggestion (Mutation)

Save AI-generated content for later use.

Type: Mutation (Write)
Access: Public (Authenticated)
Endpoint: api.suggestions.saveSuggestion

Parameters:

{
  storyId?: Id<"stories">,     // Optional: associated story
  nodeId?: Id<"nodes">,        // Optional: associated scene
  type: "improvement" | "choices" | "rewrite" | "enhance",
  originalContent: string,      // Original content that was modified
  suggestions?: string,         // Improvement suggestions text
  exampleEdits?: {              // Example improvements
    sceneTitle: string,
    revisedText: string,
    analysis: string
  },
  choices?: Array<{             // Generated story choices
    label: string,
    description: string,
    title?: string
  }>,
  content?: string,             // Rewritten/enhanced content
  note?: string                 // Optional user note
}

Example Usage:

const suggestionId = await saveSuggestion({
  storyId: "story123",
  nodeId: "node456",
  type: "improvement",
  originalContent: "The hero walked through the forest.",
  suggestions: "Add sensory details...",
  exampleEdits: {
    sceneTitle: "Into the Woods",
    revisedText: "The hero's boots crunched...",
    analysis: "Added sensory details..."
  }
});

Example Response:

"suggestion789"  // ID of saved suggestion

Error Handling:

  • Throws "Unauthorized" if user is not authenticated
  • Throws "User not found" if user doesn't exist
  • Validates that type is one of the allowed values

listMySuggestions (Query)

Retrieve saved suggestions for the current user.

Type: Query (Read-only)
Access: Public (Authenticated)
Endpoint: api.suggestions.listMySuggestions

Parameters:

{
  storyId?: Id<"stories">,  // Optional: filter by story
  nodeId?: Id<"nodes">,     // Optional: filter by scene
  type?: string             // Optional: filter by type
}

Example Usage:

const suggestions = useQuery(api.suggestions.listMySuggestions, {
  storyId: "story123",
  type: "improvement"
});

Example Response:

[
  {
    "_id": "suggestion123",
    "_creationTime": 1234567890000,
    "userId": "user456",
    "storyId": "story789",
    "nodeId": "node321",
    "type": "improvement",
    "originalContent": "The hero walked through the forest.",
    "suggestions": "Add sensory details...",
    "exampleEdits": {
      "sceneTitle": "Into the Woods",
      "revisedText": "The hero's boots crunched...",
      "analysis": "Added sensory details..."
    },
    "note": "Use this for Chapter 3"
  }
]

Error Handling:

  • Throws "Unauthorized" if user is not authenticated
  • Throws "User not found" if user doesn't exist
  • Returns empty array if no suggestions match filters

getSuggestion (Query)

Retrieve a specific saved suggestion by ID.

Type: Query (Read-only)
Access: Public (Authenticated)
Endpoint: api.suggestions.getSuggestion

Parameters:

{
  id: Id<"savedSuggestions">  // Suggestion ID to retrieve
}

Example Usage:

const suggestion = useQuery(api.suggestions.getSuggestion, {
  id: "suggestion123"
});

Example Response:

{
  "_id": "suggestion123",
  "_creationTime": 1234567890000,
  "userId": "user456",
  "type": "improvement",
  "originalContent": "The hero walked through the forest.",
  "suggestions": "Add sensory details...",
  "note": "Use this for Chapter 3"
}

Error Handling:

  • Throws "Unauthorized" if user is not authenticated or doesn't own the suggestion
  • Throws "User not found" if user doesn't exist
  • Returns null if suggestion doesn't exist

deleteSuggestion (Mutation)

Delete a saved suggestion.

Type: Mutation (Write)
Access: Public (Authenticated)
Endpoint: api.suggestions.deleteSuggestion

Parameters:

{
  id: Id<"savedSuggestions">  // Suggestion ID to delete
}

Example Usage:

await deleteSuggestion({ id: "suggestion123" });

Example Response:

null  // Success returns null

Error Handling:

  • Throws "Suggestion not found" if ID doesn't exist
  • Throws "Unauthorized" if user doesn't own the suggestion

updateSuggestionNote (Mutation)

Update the note attached to a saved suggestion.

Type: Mutation (Write)
Access: Public (Authenticated)
Endpoint: api.suggestions.updateSuggestionNote

Parameters:

{
  id: Id<"savedSuggestions">,  // Suggestion ID
  note: string                  // New note text
}

Example Usage:

await updateSuggestionNote({
  id: "suggestion123",
  note: "Perfect for the climax scene"
});

Example Response:

null  // Success returns null

Error Handling:

  • Throws "Suggestion not found" if ID doesn't exist
  • Throws "Unauthorized" if user doesn't own the suggestion

Visualization

getStoryMermaid (Query)

Generate Mermaid.js diagram code for story visualization.

Type: Query (Read-only)
Access: Public
Endpoint: api.queries.visualization.getStoryMermaid

Parameters:

{
  storyId: Id<"stories">,      // Story to visualize
  isDarkMode?: boolean         // Optional: dark mode flag
}

Example Usage:

const diagram = useQuery(api.queries.visualization.getStoryMermaid, {
  storyId: "story123",
  isDarkMode: true
});

Example Response:

{
  "storyId": "story123",
  "title": "The Dark Forest",
  "mermaid": "graph TD\n  n123[[\"🏁 Opening Scene\"]]\n  n456[\"Deep in the Woods\"]\n  n123 -->|\"Enter forest\"| n456\n\n  style n123 fill:#15803d,stroke:#22c55e,stroke-width:3px,color:#dcfce7\n",
  "nodeCount": 2,
  "edgeCount": 1
}

Error Handling:

  • Throws "Story not found" if storyId doesn't exist
  • Sanitizes all node IDs and text to prevent Mermaid syntax errors

User Management

ensureUser (Mutation)

Create or retrieve user record for authenticated session.

Type: Mutation (Write)
Access: Public (Authenticated)
Endpoint: api.ui.ensureUser

Parameters:

{}  // No parameters required

Example Usage:

await ensureUser();

Example Response:

null  // Success returns null

Error Handling:

  • Throws "Unauthorized" if user is not authenticated
  • Creates new user record if one doesn't exist
  • Returns existing user if already present

External APIs

OpenAI API

StoryForge uses OpenAI's Chat Completions API for all AI features.

Base URL: https://api.openai.com/v1
Authentication: Bearer token (API key)
Model Used: gpt-4o-mini

Chat Completions Endpoint

Method: POST
Endpoint: /v1/chat/completions
Full URL: https://api.openai.com/v1/chat/completions

Request Headers:

{
  "Content-Type": "application/json",
  "Authorization": "Bearer YOUR_API_KEY"
}

Request Body:

{
  "model": "gpt-4o-mini",
  "messages": [
    {
      "role": "system",
      "content": "You are a creative writing assistant."
    },
    {
      "role": "user",
      "content": "Improve this text: The hero walked through the forest."
    }
  ],
  "temperature": 0.8,
  "max_tokens": 1000
}

Example Success Response:

{
  "id": "chatcmpl-123",
  "object": "chat.completion",
  "created": 1677652288,
  "model": "gpt-4o-mini",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "The hero ventured through the dense forest, her footsteps muffled by a carpet of moss and fallen leaves."
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 25,
    "completion_tokens": 30,
    "total_tokens": 55
  }
}

Example Error Response:

{
  "error": {
    "message": "Incorrect API key provided",
    "type": "invalid_request_error",
    "param": null,
    "code": "invalid_api_key"
  }
}

Error Codes:

  • 401 - Invalid or missing API key
  • 429 - Rate limit exceeded
  • 500 - OpenAI server error
  • 503 - Service temporarily unavailable

StoryForge Error Handling:

  • Catches API key errors and displays setup instructions
  • Logs detailed error messages to console
  • Shows user-friendly error messages in UI
  • Retries failed requests automatically (for transient errors)

WorkOS AuthKit API

StoryForge uses WorkOS for authentication with multiple OAuth providers.

Base URL: https://api.workos.com
Authentication: Client ID and redirect URI

User Authentication Flow

Method: OAuth 2.0 Authorization Code Flow
Providers Supported:

  • Email/Password
  • Google OAuth
  • Microsoft OAuth
  • GitHub OAuth
  • Apple OAuth

Configuration:

{
  clientId: process.env.VITE_WORKOS_CLIENT_ID,
  redirectUri: process.env.VITE_WORKOS_REDIRECT_URI
}

Token Endpoint:

POST https://api.workos.com/oauth/token

JWKS Endpoint:

GET https://api.workos.com/sso/jwks/{clientId}

JWT Token Claims:

{
  "sub": "user_01234567890",
  "email": "user@example.com",
  "email_verified": true,
  "name": "John Doe",
  "picture": "https://example.com/avatar.jpg",
  "iss": "https://api.workos.com/",
  "aud": "client_id",
  "iat": 1234567890,
  "exp": 1234571490
}

StoryForge Integration:

// Authentication hook
const { user, signIn, signOut, getAccessToken } = useAuth();

// User object structure
{
  id: "user_01234567890",
  email: "user@example.com",
  firstName: "John",
  lastName: "Doe",
  emailVerified: true,
  profilePictureUrl: "https://example.com/avatar.jpg"
}

Error Handling:

  • Catches missing client ID and shows setup instructions
  • Handles token expiration with automatic refresh
  • Redirects to sign-in on authentication failures
  • Provides clear error messages for OAuth failures

API Configuration

Environment Variables

Required environment variables for API access:

# Convex
CONVEX_DEPLOYMENT=your-convex-deployment
VITE_CONVEX_URL=your-convex-url

# OpenAI (Server-side only)
OPENAI_API_KEY=sk-your-openai-api-key

# WorkOS Authentication
VITE_WORKOS_CLIENT_ID=client_your-client-id
VITE_WORKOS_REDIRECT_URI=http://localhost:5173/callback
WORKOS_CLIENT_ID=client_your-client-id  # Also needed in Convex environment

Setting Environment Variables

For Development (Convex):

npx convex env set OPENAI_API_KEY sk-your-key
npx convex env set WORKOS_CLIENT_ID client_your-id

For Frontend (.env.local):

VITE_CONVEX_URL=https://your-deployment.convex.cloud
VITE_WORKOS_CLIENT_ID=client_your-id
VITE_WORKOS_REDIRECT_URI=http://localhost:5173/callback

Rate Limits

Convex APIs

  • Query/Mutation Rate Limit: 1,000 requests per minute per user
  • Action Rate Limit: 100 requests per minute per user
  • Storage Rate Limit: 1 GB total storage per deployment
  • File Upload Limit: 10 MB per file

OpenAI API

  • Rate Limit: Depends on your OpenAI plan
  • Token Limits:
    • Request: 4,096 tokens max for gpt-4o-mini
    • Response: Configurable via max_tokens parameter
  • Retry Strategy: Exponential backoff for 429 errors

WorkOS AuthKit

  • Rate Limit: 100 requests per minute per client
  • Session Duration: Configurable, default 24 hours
  • Token Refresh: Automatic with 5-minute buffer

Best Practices

API Usage

  1. Always handle errors - Use try-catch blocks for all API calls
  2. Use TypeScript types - Leverage generated types from Convex
  3. Debounce search queries - Use useDebouncedCallback for search
  4. Cache query results - Convex automatically caches queries
  5. Validate inputs - Use Convex validators for all parameters
  6. Use proper file types - Only upload supported image formats (JPG, PNG, GIF, WebP)

Security

  1. Never expose API keys - Use environment variables
  2. Validate user permissions - Check authentication on all mutations
  3. Sanitize inputs - Escape special characters for Mermaid
  4. Use HTTPS only - All API calls use secure connections
  5. Implement rate limiting - Prevent abuse with request throttling
  6. Validate file uploads - Check file types and sizes before processing

Performance

  1. Minimize API calls - Batch operations when possible
  2. Use optimistic updates - Update UI before mutation completes
  3. Lazy load components - Only load when needed
  4. Cache AI responses - Save suggestions to database
  5. Paginate large datasets - Use Convex pagination for lists
  6. Use signed URLs - Leverage Convex's automatic URL expiration for security

Support

For API issues:

Clone this wiki locally