Architecture Overview
Reflecto follows a layered architecture built on Next.js 15 App Router with clear separation between client and server concerns.
High-Level Architecture
+-------------------+ +-------------------+ +------------------+
| | | | | |
| React Client | <---> | Next.js Server | <---> | PostgreSQL |
| (App Router) | tRPC | (API Layer) | Prisma| (Database) |
| | | | | |
+-------------------+ +-------------------+ +------------------+
| | |
v v v
+-------------------+ +---------+ +-------------+
| Zustand Stores | | Inngest | | ImageKit |
| (Client State) | | (Jobs) | | (CDN) |
+-------------------+ +---------+ +-------------+Request Flow
Every data operation follows a consistent path through three layers:
Client Component
React components call tRPC hooks (api.entry.create.useMutation()). React Query manages caching, refetching, and optimistic updates automatically.
tRPC Router
The router validates input with Zod schemas, checks authentication via protectedProcedure, applies rate limiting, and delegates to the service layer.
Service Layer
Business logic lives in dedicated service classes (EntryService, TagService, etc.). Services handle validation, extraction, and coordinate database operations via Prisma transactions.
Database
Prisma ORM executes queries against PostgreSQL. The @prisma/adapter-pg adapter enables connection pooling for serverless deployment on Vercel.
Client Component
└─> api.entry.create.useMutation()
└─> tRPC Router (Zod validation + auth check)
└─> EntryService.create()
└─> Prisma Transaction
├─> Create Entry
├─> Extract & sync Tags
├─> Extract & sync People
├─> Log Activity
└─> Update StreakAuthentication Flow
Reflecto uses NextAuth v5 with two authentication strategies:
| Strategy | Flow |
|---|---|
| Credentials | Email + password with bcrypt hashing. Requires email verification via token sent through Resend. |
| OAuth | Google and GitHub providers. Account linked to User on first sign-in. No email verification needed. |
Password reset uses the same VerificationToken model with a dedicated flow. All sessions are database-backed for server-side invalidation.
The tRPC layer exposes two procedure types:
publicProcedure— No auth required. Rate limited to 30 requests per 60 seconds.protectedProcedure— Requires valid session. Rate limited to 100 requests per 60 seconds. Guaranteesctx.session.useris non-null.
File Upload Flow
Image uploads go through a server-side pipeline:
- Client converts the file to base64 and calls
api.attachment.uploadImage - Server validates file type and size constraints
- Image is uploaded to ImageKit CDN via the Node.js SDK
- A thumbnail URL is generated automatically by ImageKit
- The
Attachmentrecord is created in the database, linked to the entry - The CDN URL is returned to the client for rendering
If the database registration fails after a successful upload, the image becomes orphaned on ImageKit. This edge case is logged for manual cleanup.
Auto-Save Mechanism
The journal editor implements debounced auto-save to prevent data loss:
- User types in the TipTap editor
- A debounce timer (typically 1-2 seconds of inactivity) triggers
- The Zustand entry store dispatches an
entry.updatemutation - React Query handles the mutation lifecycle (loading, success, error states)
- On success, the query cache is updated to reflect saved state
The editor tracks dirty state locally via Zustand, showing a save indicator in the UI. Failed saves are retried automatically by React Query.
State Management Strategy
Reflecto uses a dual-store approach:
| Layer | Tool | Purpose |
|---|---|---|
| Server State | React Query (via tRPC) | Entry data, tags, people, preferences, insights. Handles caching, refetching, pagination. |
| Client State | Zustand | UI state (sidebar open, active filters), editor state (dirty tracking), auth state, user preferences cache. |
Zustand stores are located in src/stores/:
use-auth-store— Authentication state and session infouse-entry-store— Active entry being edited, draft stateuse-preferences-store— Local preferences cache for instant UI responseuse-ui-store— Sidebar state, modals, active workspaceuse-user-store— Current user profile data
Background Jobs
Reflecto uses Inngest for reliable background job execution:
| Job | Schedule | Description |
|---|---|---|
check-inactivity-remind | Daily at 9:00 AM | Checks for users inactive for 3+ days and sends reminder emails via Resend |
Inngest provides automatic retries (3 attempts), step-based execution for reliability, and date-based idempotency keys to prevent duplicate emails.
Project Structure
src/
├── app/ # Next.js App Router (pages + API routes)
│ ├── (pages)/
│ │ ├── (protected)/ # Auth-gated routes (write, journal, reflect, etc.)
│ │ └── (public)/ # Public routes (sign-in, sign-up, verify)
│ └── api/ # API route handlers (auth, inngest)
├── server/
│ ├── api/routers/ # 13 tRPC routers
│ ├── services/ # Business logic layer
│ ├── schemas/ # Zod validation schemas
│ ├── auth/ # NextAuth configuration
│ └── utils/ # Metadata validation, error handling
├── components/ # React components (editor, layout, shared, UI)
├── stores/ # Zustand state stores
├── hooks/ # Custom React hooks
├── types/ # TypeScript type definitions
└── lib/ # Utilities (Inngest client, special dates)Route groups (protected) and (public) use Next.js layout nesting. The
protected group wraps all pages in an auth check layout that redirects
unauthenticated users to sign-in.