Skip to main content
Anchor is an offline-first, self-hostable note-taking application built with a modern tech stack. This document explains the architecture and how the different components work together.

High-Level Overview

Anchor follows a client-server architecture with offline-first capabilities:
┌─────────────────┐
│   Web Client    │
│   (Next.js)     │
└────────┬────────┘

         │  HTTP/REST

┌────────▼────────┐        ┌──────────────┐
│  Mobile Client  │───────▶│    Server    │
│   (Flutter)     │        │   (NestJS)   │
└─────────────────┘        └──────┬───────┘

                            ┌──────▼───────┐
                            │  PostgreSQL  │
                            └──────────────┘

Technology Stack

Backend (Server)

  • Framework: NestJS - A progressive Node.js framework
  • Database: PostgreSQL - Relational database
  • ORM: Prisma - Next-generation ORM
  • Authentication: JWT (JSON Web Tokens) via Passport
  • OIDC Support: OpenID Connect for SSO integration

Web Client

Mobile Client

  • Framework: Flutter - Cross-platform UI toolkit
  • State Management: Riverpod - Provider-based state management
  • Local Database: Drift - Reactive SQLite wrapper
  • Navigation: GoRouter - Declarative routing
  • HTTP Client: Dio - Powerful HTTP client
  • Rich Text Editor: Flutter Quill

Server Architecture

The server follows NestJS modular architecture:

Core Modules

server/src/
├── auth/              # Authentication & authorization
│   ├── guards/        # JWT & Admin guards
│   ├── strategies/    # Passport strategies
│   └── oidc/          # OpenID Connect integration
├── users/             # User management
├── notes/             # Note CRUD operations
│   ├── dto/           # Data transfer objects
│   └── shares/        # Note sharing logic
├── tags/              # Tag management
├── admin/             # Admin panel APIs
├── settings/          # System settings
├── prisma/            # Database service
└── tasks/             # Scheduled tasks (cleanup)

Key Features

// JWT authentication strategy
export class JwtStrategy extends PassportStrategy(Strategy) {
  validate(payload: any) {
    return { userId: payload.sub, email: payload.email };
  }
}

Database Schema

The database uses Prisma with the following key models:
  • User: User accounts and profiles
  • Note: Notes with rich text content
  • Tag: Custom tags for organization
  • NoteTag: Many-to-many relationship between notes and tags
  • Share: Note sharing permissions (viewer/editor)
  • RefreshToken: JWT refresh tokens
  • Setting: System-wide settings (OIDC, registration)

Web Architecture

The web app uses Next.js App Router with a feature-based structure:

Directory Structure

web/
├── app/                    # Next.js routes
│   ├── (auth)/             # Authentication pages
│   │   ├── login/
│   │   └── register/
│   └── (app)/              # Authenticated app pages
│       ├── notes/
│       ├── archive/
│       ├── trash/
│       └── admin/
├── features/               # Feature modules
│   ├── auth/
│   │   ├── api.ts          # API calls
│   │   ├── types.ts        # TypeScript types
│   │   ├── store.ts        # Zustand store
│   │   ├── hooks/          # React hooks
│   │   └── components/     # Feature components
│   ├── notes/
│   └── tags/
├── components/             # Shared components
│   ├── layout/             # Layout components
│   └── ui/                 # shadcn/ui primitives
└── lib/                    # Utilities
    └── api/                # API client config

Data Flow

1

User Action

User interacts with a React component
2

API Request

TanStack Query mutation/query calls the API via ky client
3

Server Processing

Next.js rewrites /api/* to the backend server
4

Cache Update

TanStack Query updates the cache and re-renders components

State Management Strategy

  • Server State: Managed by TanStack Query (notes, tags, user data)
  • UI State: Local component state with useState
  • Global State: Zustand for authentication state and preferences

Mobile Architecture

The mobile app is offline-first with automatic sync:

Architecture Pattern

┌──────────────┐
│      UI      │
│  (Widgets)   │
└──────┬───────┘

┌──────▼───────┐
│   Riverpod   │  ← State Management
│  (Providers)  │
└──────┬───────┘

┌──────▼───────┐
│ Repositories │  ← Business Logic
└──┬───────┬───┘
   │       │
   │       └──────────┐
   │                  │
┌──▼─────┐      ┌─────▼──────┐
│ Drift  │      │    Dio     │
│(SQLite)│      │   (HTTP)   │
└────────┘      └────────────┘

Feature Structure

mobile/lib/
├── core/
│   ├── database/          # Drift database
│   ├── network/           # Dio client
│   ├── router/            # GoRouter config
│   └── theme/             # App theming
└── features/
    ├── auth/
    │   ├── data/          # API & local storage
    │   ├── domain/        # Models & repositories
    │   └── presentation/  # UI & providers
    ├── notes/
    └── tags/

Offline-First Sync Strategy

1

Local-First Editing

All changes are saved to the local SQLite database first using Drift
2

Change Tracking

Modified notes are marked with isDirty and lastModifiedAt timestamps
3

Automatic Sync

When online, the app syncs using a bidirectional algorithm:
  • Client sends dirty notes with timestamps
  • Server returns conflicts and new server-side changes
  • Client resolves conflicts (server wins by default)
4

Optimistic Updates

UI updates immediately while sync happens in background

Authentication Flow

JWT Authentication

  1. User submits credentials to /api/auth/login
  2. Server validates and returns:
    • accessToken (short-lived, 15 min)
    • refreshToken (long-lived, 7 days, stored in DB)
  3. Client stores both tokens (localStorage for web, secure storage for mobile)
  4. API requests include Authorization: Bearer <accessToken>
  5. On expiry, client uses /api/auth/refresh to get new access token

OIDC Authentication

  1. Client initiates OIDC flow via /api/auth/oidc/initiate
  2. User authenticates with external provider (Pocket ID, Authelia, etc.)
  3. Provider redirects to /api/auth/oidc/callback
  4. Server exchanges code for tokens and creates/links user account
  5. Returns Anchor JWT tokens to client
OIDC supports both confidential clients (with secret) and public clients (PKCE for mobile).

API Design

The server exposes a RESTful API with the following conventions:
  • Base URL: /api
  • Authentication: Bearer token in Authorization header
  • Response Format: JSON
  • Error Format: { statusCode, message, error }

Endpoint Patterns

GET    /api/notes          # List all notes
POST   /api/notes          # Create note
GET    /api/notes/:id      # Get single note
PATCH  /api/notes/:id      # Update note
DELETE /api/notes/:id      # Soft delete note
POST   /api/notes/sync     # Bidirectional sync
See the API Reference for the complete API documentation.

Deployment Architecture

For production deployment, Anchor uses a single Docker container:
# Multi-stage build
FROM node:alpine AS builder
# Build server & web

FROM node:alpine
# Includes embedded PostgreSQL (PGlite)
# Serves Next.js static files
# Runs NestJS API
The production image:
  • Serves the Next.js web app (static files)
  • Runs the NestJS API server
  • Uses embedded PostgreSQL by default (can connect to external DB)
  • Stores data in /data volume

Security Considerations

  • Password Hashing: bcrypt with salt rounds
  • JWT Secret: Auto-generated and persisted if not provided
  • CORS: Configured per environment
  • Helmet: Security headers for Express
  • SQL Injection: Protected via Prisma ORM
  • XSS: React auto-escaping + Content Security Policy
  • OIDC: Supports PKCE for mobile public clients

Performance Optimizations

  • Database Indexes: On frequently queried fields (userId, tagId, etc.)
  • TanStack Query: Automatic caching and deduplication
  • Next.js: Static page generation and code splitting
  • Drift: Reactive streams for efficient mobile data access
  • Lazy Loading: Components and routes loaded on-demand

Next Steps

Now that you understand the architecture:

Build docs developers (and LLMs) love