Skip to content

VitekHub/cipher-note

Repository files navigation

Cipher Note

End-to-end encrypted note-taking app. The server never sees plaintext data.

Stack

  • Build: Vite
  • UI: React 19 + TypeScript
  • Styling: Tailwind CSS v4 + shadcn/ui
  • State: Zustand
  • Routing: TanStack Router (file-based)
  • Data Fetching: TanStack Query
  • i18n: react-i18next
  • Crypto: Web Crypto API + argon2-browser + @scure/bip39
  • Backend: Supabase (local Docker for dev)
  • Testing: Vitest + React Testing Library + Playwright (E2E)

Prerequisites

  • Node.js 20+ and pnpm 9+
  • Docker (required for local Supabase — runs Postgres, Auth, Realtime, and API containers)
  • Git

Getting Started

# Install dependencies
pnpm install

# Start local Supabase (Docker must be running)
pnpm supabase:start

# Copy env vars (then fill in the **Publishable** anon key from supabase status output)
cp env.local.example .env.local

# Start dev server
pnpm dev

# Run tests
pnpm test

# Run tests once
pnpm test:run

# Type check
pnpm typecheck

# Build for production
pnpm build

pnpm dev runs supabase start && vite — it starts Supabase containers if needed, then launches the Vite dev server.

Supabase Commands

pnpm supabase:start    # Start local Supabase containers
pnpm supabase:stop     # Stop containers (data preserved)
pnpm supabase:reset    # Reset database (re-runs migrations + seed)
pnpm supabase:status   # Show URLs and keys
pnpm dev:reset         # Reset database then start Vite

Architecture

src/
  app/          # Application shell (providers, router, layouts, styles)
  features/     # Feature modules (auth, fields/entries, vault, settings)
  shared/       # Shared code (ui, crypto, api, auth, i18n, types)

Dependency direction: routes -> features -> shared. No cross-feature imports.

App Hierarchy

┌────────────┐
│ index.html │
└─────┬──────┘
      ▼
┌───────────┐
│  main.tsx │──▶ i18n init
│           │──▶ Tailwind CSS
└─────┬─────┘
      ▼
┌───────────────────┐
│  AppProviders     │
│  ┌──────────────┐ │
│  │ QueryClient  │ │
│  │ ┌──────────┐ │ │
│  │ │  Auth    │ │ │
│  │ │ ┌──────┐ │ │ │
│  │ │ │Router│ │ │ │
│  │ │ └──┬───┘ │ │ │
│  │ └────┬─────┘ │ │
│  └──────┼───────┘ │
└─────────┼─────────┘
          ▼
    ┌────────────┐
    │  __root    │──▶ ThemeProvider + Toaster
    └─────┬──────┘
      ┌───┴───┐
      ▼       ▼
  _public   _authenticated
  (guest)   (logged in)
      │       │
      ▼       ▼
  /login    /dashboard (redirects to first entry)
  /register /dashboard/$entryId (entry detail)
  /recover  /settings

Key Hierarchy

Cipher Note uses a layered key hierarchy where each layer protects the one below. The server never sees plaintext keys — only wrapped (encrypted) key material.

                          User Password
                               │
                     (never sent to server)
                               │
                   ┌───────────┴───────────┐
                   │ Split KDF (Argon2id)  │
                   └───────────┬───────────┘
                               │
                 ┌─────────────┴───────────────┐
                 │                             │
            HKDF │                             │ HKDF
                 │                             │
                 ▼                             ▼
            authHash                    passwordKey
                 │                             │
sent to Supabase │                             │ kept client-side only
   as "password" │                             │ (never sent to server)
                 │                             │
                 ▼                             │ unwraps
           ┌──────────┐                        │
           │ Supabase │                        ▼
           │   Auth   │                ┌────────────┐
           └──────────┘                │ Master Key │ (random 256 bits)
                                       └──┬─────┬───┘
                                          │     │
                                     HKDF │     │ HKDF
                                          │     │
                                          ▼     ▼
                                      ┌─────┐ ┌──────────────────┐
               (never sent to server) │ KEK │ │ Signing Key Seed │ (never sent to server)
                                      └──┬──┘ └──────────────────┘
                                         │
                            AES-GCM wrap │
                                         ▼
                               ┌─────────────────┐
                               │   Field Keys    │
                               │ (one per field) │
                               └────────┬────────┘
                                        │
                         AES-256-GCM encrypt/decrypt
                         user field data per entry


  ┌───────────────────────────────────────────────┐
  │           Recovery Path (BIP-39)              │
  │                                               │
  │  12-word mnemonic (never sent to server)      │
  │       │                                       │
  │       │ Argon2id                              │
  │       ▼                                       │
  │  recovery KEK (never sent to server)          │
  │       │                                       │
  │       │ AES-256-GCM                           │
  │       ▼                                       │
  │  wrapped master key                           │
  │  (independent from password wrapping —        │
  │   allows password change without              │
  │   re-encrypting field keys)                   │
  └───────────────────────────────────────────────┘

Project Conventions

  • No barrel files (index.ts). Import directly by path: import { Button } from '@/shared/ui/button'
  • Target 100-200 lines per file, max 300. Split large files into focused modules.
  • Dark theme is the default. The <html> element has class="dark".
  • Lazy-load heavy crypto modules. argon2-browser and @scure/bip39 are dynamically imported, never top-level.
  • Each shadcn component in its own file. No index.ts in shared/ui. Custom components are organized into subdirectories: brand/, nav/, form/.
  • Types in separate .types.ts files. Keep type definitions separate from implementation.
  • Tests are colocated with code. button.tsx -> button.test.tsx in the same directory.
  • File naming. React components use PascalCase (LoginPage.tsx, InputField.tsx). Utilities, hooks, schemas, and types use kebab-case (auth-store.ts, login-schema.ts). shadcn/ui primitives stay kebab-case as generated (button.tsx, card.tsx, skeleton.tsx).

Environment Variables

Copy env.local.example to .env.local and fill in values:

cp env.local.example .env.local

After running pnpm supabase:start, copy the Publishable key from the output into .env.local as VITE_SUPABASE_ANON_KEY.

License

MIT

About

A private, zero-knowledge, E2EE notes app

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors