ProductFlo.io

Overview

Backend is powered by Convex functions for locking, versioning, conflicts, comments, and real-time collaboration. Call functions from the browser using the Convex client; there is no REST surface.

How to call: Use identifiers like files:finalizeUpload, locks:get, versions:list.

Auth: Authentication is via Clerk. See config:auth below and the meta-based Clerk setup in the app.

Auth & Session

TypeIdentifierDescription
QUERYconfig:authReturns { requiresInvitation: boolean }
N/AClerk JSLoaded via CDN by /js/clerk-loader.js using meta tags clerk-publishable-key and clerk-frontend-api.

Invitations

TypeIdentifierArgsNotes
MUTATION invitations:send { email, invitedBy?, workspace? } Creates or reuses a pending invite.
QUERY invitations:list { status?, workspace?, limit? } Filter by status all|pending|accepted.
QUERY invitations:validate { invitationCode } Pre-auth validation for signup flows.
MUTATION invitations:accept { invitationCode } Marks the invite as accepted.

Workspaces & Presence

TypeIdentifierArgsDescription
QUERYworkspaces:list{}List all workspaces
MUTATIONworkspaces:create{ name, creatorEmail?, invitedBy? }Create a workspace (auto-add creator)
QUERYworkspaces:listForEmail{ email }List workspaces a user belongs to
MUTATIONpresence:heartbeat{ workspace, username }Mark user online
QUERYpresence:list{ workspace }List online users

Files & Uploads (Convex)

TypeIdentifierArgsDescription
ACTIONfiles:generateUploadUrl{}Get a one-time upload URL
MUTATIONfiles:finalizeUpload{ workspace, owner, path, storageId, baseVersion?, size?, contentType?, sha256?, lockPath? }Finalize upload; detects conflicts
ACTIONfiles:getDownloadUrl{ storageId }Temporary download URL
QUERYfiles:list{ workspace, owner? }List files
MUTATIONfiles:remove{ workspace, path }Soft-delete (Trash)

Example: Upload with baseVersion

// Using Convex browser client
const convex = getConvexClient();
const { url } = await convex.action('files:generateUploadUrl', {});
await fetch(url, { method: 'POST', headers: { 'Content-Type': file.type }, body: file });
await convex.mutation('files:finalizeUpload', {
  workspace: 'default', owner: 'alice', path: `default/alice/${file.name}`,
  storageId, baseVersion: 'PREVIOUS_ETAG', contentType: file.type, lockPath: `default/alice/${file.name}`
});

History & Conflicts (Convex)

TypeIdentifierArgsDescription
QUERYversions:list{ path }List versions for a file
MUTATIONversions:revert{ path, versionId, author }Revert to a previous version
QUERYconflicts:list{ workspace }List active conflicts
MUTATIONconflicts:resolve{ workspace, baseName, acceptedPath, author }Resolve a conflict

Comments (Convex)

TypeIdentifierArgsDescription
QUERYcomments:list{ path, limit? }List comments
MUTATIONcomments:create{ path, author, body }Add a comment

Locks (Convex)

TypeIdentifierArgsDescription
QUERYlocks:get{ path }Check current lock
MUTATIONlocks:lock{ path, owner, ttlMs? }Acquire/refresh a lock
MUTATIONlocks:unlock{ path, lockId }Release a lock
MUTATIONlocks:requestEdit{ path, from }Ask current owner for edit access
QUERYlocks:listRequests{ workspace, to, sinceMs? }Subscribe for incoming edit requests

Trash

TypeIdentifierArgsDescription
QUERYtrash:list{ workspace, owner, limit? }List soft-deleted items
MUTATIONtrash:restore{ id }Restore a file

Admin & Users

TypeIdentifierArgsDescription
MUTATIONusers:syncFromIdentity{ email?, username?, isAnonymous? }Upsert current user
QUERYusers:getByEmail{ email }Fetch user record

Errors & Status Codes

  • 400: Missing or invalid parameters
  • 401/403: Unauthorized (protected routes)
  • 404: Not found
  • 409/423: Conflict or Locked (locks, uploads)
  • 413: Payload too large (upload)
  • 415: Unsupported type (text preview)
  • 429: Rate limited
  • 500: Server error
  • 503: Database unavailable (user endpoints)