Skip to Content
Welcome to Reflecto Docs - Your guide to mindful journaling
Developer GuideAPI Reference

API Reference

Reflecto exposes a type-safe API through tRPC v11. All procedures are accessed via the api client — there are no REST endpoints to document. The tRPC client provides full TypeScript inference from server to client.

Router Overview

The root router combines 13 domain-specific routers:

RouterProceduresAuthDescription
entry7ProtectedCore CRUD, insights stats, memory lane
journalProtectedJournal-specific queries
dreamProtectedDream-specific queries
highlightProtectedHighlight-specific queries
ideaProtectedIdea-specific queries
wisdomProtectedWisdom-specific queries
noteProtectedNote-specific queries
tag4ProtectedTag CRUD and search
person4ProtectedPerson CRUD and search
attachment3ProtectedImage upload and management
insights3ProtectedHeatmap, streaks, entry stats
preferences2ProtectedUser settings get/update
userProtectedProfile management

Authentication

Every procedure uses one of two base procedures:

// Public -- no auth required, 30 req/60s rate limit export const publicProcedure = t.procedure .use(timingMiddleware) .use(rateLimitMiddleware); // Protected -- auth required, 100 req/60s rate limit export const protectedProcedure = t.procedure .use(timingMiddleware) .use(rateLimitMiddleware) .use(authMiddleware); // throws UNAUTHORIZED if no session

Protected procedures guarantee ctx.session.user is non-null. All data-modifying operations are protected.

Rate Limiting

The API uses an in-memory sliding window rate limiter:

TierLimitWindowKey
Authenticated100 requests60 secondsuser:{userId}
Unauthenticated30 requests60 secondsip:{x-forwarded-for}

Exceeding the limit returns a TOO_MANY_REQUESTS error. The store is cleaned up every 5 minutes.

Entry Operations

entry.create

Creates a new entry with automatic tag and people extraction.

Type: Mutation (protected)

Input:

{ type: "journal" | "dream" | "highlight" | "idea" | "wisdom" | "note", title?: string, // max 200 characters content?: string, // max 50,000 characters (HTML) isStarred?: boolean, editorMode?: string, // "bullet" | "simple" metadata?: Record<string, unknown>, createdAt?: Date, // backdate support }

Response: Full entry with tags, people, and attachments relations.

Side effects:

  • Tags extracted from #hashtag patterns and synced
  • People extracted from @mention patterns and synced
  • ActivityLog updated for the day
  • Streak recalculated
  • Journal entries enforce one-per-day (returns existing if duplicate)

Pagination Pattern

All list endpoints use cursor-based pagination:

// First page const { entries, nextCursor } = await api.entry.list.useQuery({ type: "journal", limit: 20, }); // Next page const page2 = await api.entry.list.useQuery({ type: "journal", limit: 20, cursor: nextCursor, });

The implementation fetches limit + 1 items. If the extra item exists, it becomes the nextCursor. When nextCursor is undefined, there are no more pages.

Tag and Person Endpoints

ProcedureTypeInputDescription
tag.listQueryAll tags for the user with entry counts
tag.searchQuery{ query: string }Search tags by name
tag.updateMutation{ id, name?, color?, group? }Update tag properties
tag.deleteMutation{ id: string }Delete a tag and remove associations

Tags and people are primarily created automatically through content extraction. These endpoints manage existing records — renaming, grouping, and cleanup.

Attachment Endpoints

ProcedureTypeDescription
attachment.uploadImageMutationUpload base64 image to ImageKit, optionally link to entry
attachment.registerImageAttachmentMutationRegister an already-uploaded image in the database
attachment.deleteMutationDelete attachment record and remove from CDN

Upload input:

{ fileData: string, // base64-encoded image data fileName: string, // original filename fileType: string, // MIME type fileSize: number, // size in bytes entryId?: string, // link to entry (optional) }

Insights Endpoints

ProcedureTypeInputDescription
insights.getHeatmapQuery{ from?, to? }Activity heatmap data (default: last year)
insights.getStreakQueryCurrent streak length and longest streak
insights.getStatsQueryEntry count grouped by type

Preferences Endpoints

ProcedureTypeDescription
preferences.getQueryFetch current user preferences
preferences.updateMutationUpdate theme, font size, notifications, etc.

Error Handling

tRPC errors use standard codes with Zod validation details:

CodeWhen
UNAUTHORIZEDNo valid session on protected procedure
NOT_FOUNDEntry/tag/person does not exist or wrong owner
TOO_MANY_REQUESTSRate limit exceeded
BAD_REQUESTZod validation failure
INTERNAL_SERVER_ERRORUnhandled service error

Validation errors include the flattened Zod error for field-level feedback:

{ data: { zodError: { fieldErrors: { title: ["String must contain at most 200 character(s)"]; } } } }

Client Usage

import { api } from "@/trpc/react"; // Query with React Query integration const { data, isLoading } = api.entry.list.useQuery({ type: "journal", limit: 20, }); // Mutation with callbacks const createEntry = api.entry.create.useMutation({ onSuccess: () => { // Invalidate and refetch entry lists utils.entry.list.invalidate(); }, }); // Trigger mutation createEntry.mutate({ type: "journal", title: "My Entry", content: "<p>Today I learned about #typescript with @Sarah</p>", metadata: { category: "Learning", mood: 4 }, });

All dates are serialized through SuperJSON, so you can pass native Date objects in inputs and receive them in responses without manual conversion.

Last updated on