Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/arrozet/caret/llms.txt

Use this file to discover all available pages before exploring further.

Caret organizes your writing into a clear three-level hierarchy: Workspaces at the top, Folders nested within them, and Documents living inside folders or directly at the workspace root. Every entity is UUID-keyed, soft-deleted, and protected by Supabase Row-Level Security policies at the database layer — so access control is enforced even if application-level checks are bypassed. The Document Service is the single authoritative backend for all create, read, update, and delete operations on this hierarchy.

Entity Hierarchy

Workspace
└── Folder (nested, arbitrary depth)
    └── Document
Document (at workspace root, no folder)

Workspace

The top-level shared context for a team. Members have roles that are inherited by documents. Every user gets a default personal workspace on signup.

Folder

Nestable containers within a workspace. Folders can contain other folders for arbitrary depth organization.

Document

The leaf entity. A document belongs to exactly one workspace and optionally one folder. It can also be shared directly with specific users independent of workspace membership.

Workspaces

Personal vs. Shared

Every user gets a personal workspace automatically on signup. Personal workspaces are single-user by default but can be upgraded by moving documents to a shared workspace. Shared workspaces support team membership with role-based access. The editor toolbar shows a “Move to workspace” action when the current document is in a personal workspace and at least one shared workspace exists.

Workspace Membership Roles

Workspace roles control what a member can do within the workspace itself. These roles are distinct from per-document roles.
RolePermissions
ownerFull control — manage members, rename, delete workspace
adminManage members and workspace settings
memberCreate and edit documents in the workspace
guestLimited read-only access to the workspace
Access granted at the workspace level is inherited by all documents in that workspace. When a new user is invited to a workspace via the invite endpoint, they receive the member role by default.

Workspace API Endpoints

POST   /api/v1/workspaces              — Create a new workspace
GET    /api/v1/workspaces              — List workspaces for the authenticated user
GET    /api/v1/workspaces/{id}         — Get a single workspace by ID
PATCH  /api/v1/workspaces/{id}         — Update workspace name or settings
DELETE /api/v1/workspaces/{id}         — Soft-delete a workspace
POST   /api/v1/workspaces/{id}/invite  — Invite a user by email

Folders

Folders provide nested organization within a workspace. There is no enforced depth limit — folders can contain other folders for as many levels as your project requires. Documents can live:
  • Inside a folderfolder_id is set to the folder’s UUID
  • At the workspace rootfolder_id is null

Folder API Endpoints

POST   /api/v1/folders          — Create a folder
GET    /api/v1/folders          — List folders (scoped to a workspace)
GET    /api/v1/folders/all      — List all folders the user can access (flat, for tree building)
GET    /api/v1/folders/{id}     — Get a single folder by ID
PATCH  /api/v1/folders/{id}     — Rename or move a folder
DELETE /api/v1/folders/{id}     — Soft-delete a folder

Documents

Document Sharing

In addition to workspace-level access, documents support direct per-document sharing with individual users. This is useful for sharing a single document with someone outside the workspace. Document-level roles are independent of workspace roles:
RolePermissions
ownerFull control — edit, share, delete
editorEdit document content
commenterAdd comments; read-only for content
viewerRead-only
Direct shares are managed through the invite endpoint in the editor’s Share dialog:
POST /api/v1/documents/{id}/invite
Content-Type: application/json

{ "email": "collaborator@example.com" }

Document API Endpoints

POST   /api/v1/documents              — Create a new document
GET    /api/v1/documents              — List documents (filtered by workspace/folder)
GET    /api/v1/documents/shared       — List documents shared directly with the user
GET    /api/v1/documents/{id}         — Get a single document with its content
PATCH  /api/v1/documents/{id}         — Update title, content, folder, or workspace
DELETE /api/v1/documents/{id}         — Soft-delete a document
POST   /api/v1/documents/{id}/invite  — Share document with a user by email

Document Content Storage

Document content is stored in two complementary formats in the same row:
ColumnFormatPurpose
content_jsonTiptap / ProseMirror JSONPreserves rich text formatting, node structure, and marks for round-trip editor fidelity
content_textPlain textUsed for full-text search, AI embedding indexing, and the status bar’s word/character counts
Both columns are written on every autosave. The AI service reads content_text for embedding generation; the editor rehydrates from content_json on load.

Document Versions

Every save can produce a version record in the document_versions table, which stores a snapshot of content_json and content_text at that point in time. This gives you a full content history for version management and rollback.
Version history is managed by the Document Service. The editor displays the current version and provides UI to browse and restore previous versions when available.

Soft Deletes

No content in Caret is ever hard-deleted unless explicitly required. All entities — workspaces, folders, and documents — use a deleted_at timestamp column:
  • Active: deleted_at IS NULL
  • Soft-deleted: deleted_at is set to the deletion timestamp
This pattern means:
  • Accidental deletions can be recovered
  • Referential integrity is preserved (foreign keys still resolve)
  • Audit trails remain intact
Queries that list workspaces, folders, or documents always filter on deleted_at IS NULL. If you are writing a custom query against the Supabase database directly, remember to include this filter to exclude soft-deleted records.

UUID Primary Keys

All entities use UUID primary keys generated by PostgreSQL’s gen_random_uuid() function:
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
UUIDs ensure:
  • No sequential ID enumeration attacks
  • Safe distributed generation without coordination
  • Consistent key format across all services

Row-Level Security

Access control in Caret is enforced at two layers:
1

Application layer

The Document Service validates the authenticated user’s role before executing any create, read, update, or delete operation. Unauthorized requests receive 403 Forbidden before touching the database.
2

Database layer (RLS)

Supabase Row-Level Security policies are applied at the PostgreSQL level. Even if a query reaches the database without passing through the application-layer checks, RLS ensures that rows outside the user’s access scope are invisible and unwritable.
This defense-in-depth approach means that no misconfigured query or service-to-service call can accidentally expose another user’s documents.
Row-Level Security policies in Supabase are PostgreSQL CREATE POLICY statements attached to each table. They evaluate against the authenticated user’s JWT claims (specifically the sub / user ID) on every query. For example, a policy on the documents table might look like:
CREATE POLICY "Users can read their own documents"
  ON documents FOR SELECT
  USING (
    owner_user_id = auth.uid()
    OR EXISTS (
      SELECT 1 FROM document_members
      WHERE document_id = documents.id
        AND user_id = auth.uid()
    )
  );
Policies are set once at the database level and apply universally, regardless of which service or client issues the query.

Architecture: Document Service

All workspace, folder, and document CRUD is owned by the Document Service (document-service), an Express 5 + TypeScript microservice using Drizzle ORM.
LayerResponsibility
RoutesParse HTTP boundary, validate inputs, call services
ServicesBusiness rules, role checks, mapping
RepositoriesAll SQL via Drizzle — no HTTP concepts
DTOsTyped request/response shapes
The API Gateway proxies all /api/v1/documents, /api/v1/workspaces, and /api/v1/folders traffic to the Document Service. Frontend HTTP calls always go through the gateway — never directly to the service on its internal port.

Build docs developers (and LLMs) love