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

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 Streak

Authentication Flow

Reflecto uses NextAuth v5 with two authentication strategies:

StrategyFlow
CredentialsEmail + password with bcrypt hashing. Requires email verification via token sent through Resend.
OAuthGoogle 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. Guarantees ctx.session.user is non-null.

File Upload Flow

Image uploads go through a server-side pipeline:

  1. Client converts the file to base64 and calls api.attachment.uploadImage
  2. Server validates file type and size constraints
  3. Image is uploaded to ImageKit CDN via the Node.js SDK
  4. A thumbnail URL is generated automatically by ImageKit
  5. The Attachment record is created in the database, linked to the entry
  6. 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:

  1. User types in the TipTap editor
  2. A debounce timer (typically 1-2 seconds of inactivity) triggers
  3. The Zustand entry store dispatches an entry.update mutation
  4. React Query handles the mutation lifecycle (loading, success, error states)
  5. 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:

LayerToolPurpose
Server StateReact Query (via tRPC)Entry data, tags, people, preferences, insights. Handles caching, refetching, pagination.
Client StateZustandUI 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 info
  • use-entry-store — Active entry being edited, draft state
  • use-preferences-store — Local preferences cache for instant UI response
  • use-ui-store — Sidebar state, modals, active workspace
  • use-user-store — Current user profile data

Background Jobs

Reflecto uses Inngest for reliable background job execution:

JobScheduleDescription
check-inactivity-remindDaily at 9:00 AMChecks 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.

Last updated on