experiments in a post-browser web
peek apps server ARCHITECTURE.md
4.1 kB · 147 lines
Markdown
at main

Server Architecture #

Overview #

The Peek sync server is a Node.js/Hono application that supports both multi-user (hosted) and single-user (self-hosted) deployments. It uses abstraction layers for SQL and storage to enable future migration to different backends.

Abstraction Layers #

SQL Abstraction (sql/) #

Abstracts database operations to support different SQLite backends:

sql/
├── types.js          # SqlAdapter and SqlAdapterFactory interfaces
├── node-sqlite-adapter.js  # Default adapter for Node.js (node:sqlite, built in)
└── index.js          # Factory that selects adapter based on SQL_ADAPTER env

Interface:

interface SqlAdapter {
  exec(sql: string): void;
  run(sql: string, params?: unknown[]): { changes: number, lastInsertRowid?: number };
  get<T>(sql: string, params?: unknown[]): T | null;
  all<T>(sql: string, params?: unknown[]): T[];
  transaction<T>(fn: () => T): T;
  close(): void;
}

Future adapters:

  • do-sqlite-adapter.js - For Cloudflare Durable Objects (TODO)

Storage Abstraction (storage/) #

Abstracts image/blob storage to support different backends:

storage/
├── types.js          # StorageAdapter interface
├── filesystem-adapter.js  # Default adapter for local filesystem
└── index.js          # Factory that selects adapter based on STORAGE_BACKEND env

Interface:

interface StorageAdapter {
  put(key: string, data: Buffer, metadata?: object): Promise<void>;
  putSync(key: string, data: Buffer, metadata?: object): void;
  get(key: string): Promise<Buffer | null>;
  getSync(key: string): Buffer | null;
  delete(key: string): Promise<void>;
  deleteSync(key: string): void;
  exists(key: string): Promise<boolean>;
  existsSync(key: string): boolean;
  list(prefix?: string): Promise<string[]>;
}

Future adapters:

  • r2-adapter.js - For Cloudflare R2 (TODO)
  • s3-adapter.js - For S3-compatible storage (TODO)

User Modes #

Multi-User Mode (Default) #

Standard hosted deployment with user isolation:

DATA_DIR/
├── system.db                    # User registry
├── {userId}/
│   └── profiles/
│       └── {profileId}/
│           ├── datastore.sqlite
│           └── images/
└── backups/
  • Authentication via API key (SHA-256 hash lookup in system.db)
  • Full user/profile management endpoints
  • Data isolation between users

Single-User Mode #

Simplified self-hosted deployment:

DATA_DIR/
└── profiles/
    └── {profileId}/
        ├── datastore.sqlite
        └── images/

Enable with:

SINGLE_USER_MODE=true
SINGLE_USER_TOKEN=your-secret-token  # Optional bearer token
SINGLE_USER_ID=default               # Optional, defaults to 'default'

Simplifications:

  • No system.db (no user registry)
  • No API key hashing (simple token comparison)
  • No userId in paths
  • User management endpoints disabled

Configuration #

Variable Default Description
PORT 3000 Server port
DATA_DIR ./data Data directory
SQL_ADAPTER node-sqlite SQL adapter to use
STORAGE_BACKEND filesystem Storage backend to use
SINGLE_USER_MODE false Enable single-user mode
SINGLE_USER_TOKEN (none) Bearer token for single-user auth
SINGLE_USER_ID default User ID in single-user mode

Files #

File Purpose
index.js Hono app, routes, middleware
db.js Database operations, connection pool
users.js User/profile management (system.db)
backup.js Backup/restore functionality
config.js Configuration loading
auth.js Authentication middleware factory
sql/ SQL abstraction layer
storage/ Storage abstraction layer

Testing #

# Unit tests (110 tests)
yarn server:test

# Sync E2E tests (13 tests)
yarn test:sync:e2e

# Single-user mode tests
SINGLE_USER_MODE=true yarn server:test