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:
| Router | Procedures | Auth | Description |
|---|---|---|---|
entry | 7 | Protected | Core CRUD, insights stats, memory lane |
journal | — | Protected | Journal-specific queries |
dream | — | Protected | Dream-specific queries |
highlight | — | Protected | Highlight-specific queries |
idea | — | Protected | Idea-specific queries |
wisdom | — | Protected | Wisdom-specific queries |
note | — | Protected | Note-specific queries |
tag | 4 | Protected | Tag CRUD and search |
person | 4 | Protected | Person CRUD and search |
attachment | 3 | Protected | Image upload and management |
insights | 3 | Protected | Heatmap, streaks, entry stats |
preferences | 2 | Protected | User settings get/update |
user | — | Protected | Profile 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 sessionProtected 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:
| Tier | Limit | Window | Key |
|---|---|---|---|
| Authenticated | 100 requests | 60 seconds | user:{userId} |
| Unauthenticated | 30 requests | 60 seconds | ip:{x-forwarded-for} |
Exceeding the limit returns a TOO_MANY_REQUESTS error. The store is cleaned up every 5 minutes.
Entry Operations
Create
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
#hashtagpatterns and synced - People extracted from
@mentionpatterns 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
Tags
| Procedure | Type | Input | Description |
|---|---|---|---|
tag.list | Query | — | All tags for the user with entry counts |
tag.search | Query | { query: string } | Search tags by name |
tag.update | Mutation | { id, name?, color?, group? } | Update tag properties |
tag.delete | Mutation | { 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
| Procedure | Type | Description |
|---|---|---|
attachment.uploadImage | Mutation | Upload base64 image to ImageKit, optionally link to entry |
attachment.registerImageAttachment | Mutation | Register an already-uploaded image in the database |
attachment.delete | Mutation | Delete 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
| Procedure | Type | Input | Description |
|---|---|---|---|
insights.getHeatmap | Query | { from?, to? } | Activity heatmap data (default: last year) |
insights.getStreak | Query | — | Current streak length and longest streak |
insights.getStats | Query | — | Entry count grouped by type |
Preferences Endpoints
| Procedure | Type | Description |
|---|---|---|
preferences.get | Query | Fetch current user preferences |
preferences.update | Mutation | Update theme, font size, notifications, etc. |
Error Handling
tRPC errors use standard codes with Zod validation details:
| Code | When |
|---|---|
UNAUTHORIZED | No valid session on protected procedure |
NOT_FOUND | Entry/tag/person does not exist or wrong owner |
TOO_MANY_REQUESTS | Rate limit exceeded |
BAD_REQUEST | Zod validation failure |
INTERNAL_SERVER_ERROR | Unhandled 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.