-
Notifications
You must be signed in to change notification settings - Fork 2
API Documentation
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.
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
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 IDError 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
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
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 nullError Handling:
- Throws error if nodeId doesn't exist
- Validates that content is a string
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 nullError Handling:
- Throws error if nodeId doesn't exist
- Validates that title is a string
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 nodeError 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
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 nullError 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
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 nullError Handling:
- Throws error if edgeId doesn't exist
- No validation on whether edge is in use
Generate a secure upload URL for an image file.
Type: Mutation (Write)
Access: Public (Authenticated)
Endpoint: api.image.generateUploadUrl
Parameters:
{} // No parameters requiredExample Usage:
const uploadUrl = await generateUploadUrl();Example Response:
"https://convex-cloud-storage.s3.amazonaws.com/upload/..." // Secure upload URLError Handling:
- Returns a signed URL that expires after a short time
- URL is single-use only
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 nullError 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
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 nullError Handling:
- Throws "Node not found" if nodeId doesn't exist
- Deletes image from storage if it exists
- Removes reference from node
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 URLError Handling:
- Returns null if node doesn't exist
- Returns null if node has no image
- Signed URL expires after a period of time
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
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 sessionError 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
Get all play sessions for the current user.
Type: Query (Read-only)
Access: Public (Authenticated)
Endpoint: api.ui.listMySessions
Parameters:
{} // No parameters requiredExample 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
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
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 nullError 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
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 availableError Handling:
- Throws "Unauthorized" if user is not authenticated
- Throws "No access" if user is not a participant
- Returns null if no choices are available
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
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
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
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
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 suggestionError 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
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
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
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 nullError Handling:
- Throws "Suggestion not found" if ID doesn't exist
- Throws "Unauthorized" if user doesn't own the suggestion
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 nullError Handling:
- Throws "Suggestion not found" if ID doesn't exist
- Throws "Unauthorized" if user doesn't own the suggestion
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
Create or retrieve user record for authenticated session.
Type: Mutation (Write)
Access: Public (Authenticated)
Endpoint: api.ui.ensureUser
Parameters:
{} // No parameters requiredExample Usage:
await ensureUser();Example Response:
null // Success returns nullError Handling:
- Throws "Unauthorized" if user is not authenticated
- Creates new user record if one doesn't exist
- Returns existing user if already present
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
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)
StoryForge uses WorkOS for authentication with multiple OAuth providers.
Base URL: https://api.workos.com
Authentication: Client ID and redirect URI
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
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 environmentFor Development (Convex):
npx convex env set OPENAI_API_KEY sk-your-key
npx convex env set WORKOS_CLIENT_ID client_your-idFor 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- 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
- Rate Limit: Depends on your OpenAI plan
-
Token Limits:
- Request: 4,096 tokens max for gpt-4o-mini
- Response: Configurable via
max_tokensparameter
- Retry Strategy: Exponential backoff for 429 errors
- Rate Limit: 100 requests per minute per client
- Session Duration: Configurable, default 24 hours
- Token Refresh: Automatic with 5-minute buffer
- Always handle errors - Use try-catch blocks for all API calls
- Use TypeScript types - Leverage generated types from Convex
-
Debounce search queries - Use
useDebouncedCallbackfor search - Cache query results - Convex automatically caches queries
- Validate inputs - Use Convex validators for all parameters
- Use proper file types - Only upload supported image formats (JPG, PNG, GIF, WebP)
- Never expose API keys - Use environment variables
- Validate user permissions - Check authentication on all mutations
- Sanitize inputs - Escape special characters for Mermaid
- Use HTTPS only - All API calls use secure connections
- Implement rate limiting - Prevent abuse with request throttling
- Validate file uploads - Check file types and sizes before processing
- Minimize API calls - Batch operations when possible
- Use optimistic updates - Update UI before mutation completes
- Lazy load components - Only load when needed
- Cache AI responses - Save suggestions to database
- Paginate large datasets - Use Convex pagination for lists
- Use signed URLs - Leverage Convex's automatic URL expiration for security
For API issues:
- Convex: https://docs.convex.dev
- OpenAI: https://platform.openai.com/docs
- WorkOS: https://workos.com/docs