Menu
Penombre docs

These are the canary docs, read from main: they can describe changes no release has shipped yet. Read the v1.8.66 docs.

Project architecture

Overview of the Penombre project architecture.

#Overview

Penombre is structured as a Bun monorepo with two packages:

  • Web — SvelteKit full-stack app (frontend + REST API).
  • Docs — SvelteKit static documentation site.
penombre/
├── packages/
│   ├── web/       # SvelteKit — frontend + REST API + database
│   └── docs/      # SvelteKit — documentation
├── Dockerfile     # Multi-stage production build
└── compose.yaml   # Docker Compose (single app container)

#Web package

The web package is the core of Penombre — a SvelteKit application that serves both the frontend UI and the backend REST API.

#Tech stack

LayerTechnology
FrameworkSvelteKit + Svelte 5
RuntimeBun (via @sveltejs/adapter-bun)
StylingTailwindCSS 4, shadcn-svelte (bits-ui)
ORMDrizzle ORM on SQLite (PostgreSQL optional)
AuthBetter Auth (email/password, OAuth, passkeys, API keys)
i18nParaglide-JS (13 languages, project.inlang)
Formssveltekit-superforms + Valibot
APIOpenAPI-first with Zod schemas

#Backend architecture

src/lib/server/
├── openapi/v1/    # API schema definitions (Zod + defineRoute)
├── services/      # Business logic
│   ├── storage/   # File/folder CRUD, caching, thumbnails
│   ├── activity.ts
│   ├── preferences.ts
│   └── user.ts
├── auth/          # Better Auth setup & seeding
├── db/            # Drizzle schema & connection
├── config.ts      # Runtime configuration (Zod-validated)
├── errors.ts      # Custom error classes
└── http.ts        # Standardized HTTP response helpers

#OpenAPI-first API

API routes follow the thin handler pattern. Schemas are defined in src/lib/server/openapi/v1/ using defineRoute() with Zod, and route handlers in src/routes/api/v1/ delegate all business logic to the service layer.

// Route handler — thin wrapper
export const POST = createFile.handler(async ({ body, query, service }) => {
  const res = await service.createFile(body, query.folder);
  return Http.Ok(res);
});

All API responses use a standardized format via Http.* helpers:

{ "data": { ... }, "message": "..." }

#Service layer

Services encapsulate business logic and are instantiated per-request with the authenticated user context. The main services are:

  • StorageService — file/folder CRUD, uploads, downloads, sharing, metadata caching
  • ActivityService — audit logging (create, update, delete, share, rename actions)
  • PreferencesService — user layout and sorting settings
  • UserService — profile management

#Authentication

Better Auth handles authentication with the following plugins:

  • Email/password — traditional sign-in with optional email verification via SMTP
  • OAuth — any OIDC-compliant provider (Google, GitHub, Pocket ID, etc.)
  • Passkeys — WebAuthn/FIDO2 passwordless authentication
  • API keys — rate-limited keys for programmatic access

On startup, the server hook (hooks.server.ts) waits for the database, runs Drizzle migrations, and seeds the initial admin account if no users exist.

#Rate limits

Sign-in lookups, share-link passwords and browser error reports are rate-limited. The counters live in memory, one set per app instance; with REDIS_URL set they live in Redis and are shared by every instance. See Environment variables.

Listings are not cached: they are keyset-paginated SQL on indexed columns, a few milliseconds even for a folder of twenty thousand files, and always read the current rows.

#Configuration

Runtime configuration is loaded from environment variables and validated with Zod at startup. Defaults are defined in config.defaults.ts. See Environment variables for the full reference.

#Frontend architecture

src/lib/
├── components/
│   ├── ui/           # 40+ shadcn-svelte components
│   └── ...           # File views (grid, list, table), dialogs, etc.
├── store/            # Svelte stores
│   ├── actions.ts    # File/folder action state
│   ├── music.ts      # Audio player state
│   ├── sorting.ts    # Layout & sorting preferences
│   ├── title.ts      # Page title
│   └── upload.ts     # Upload progress tracking
├── schemas/          # Form validation (sveltekit-superforms)
├── paraglide/        # Generated i18n
└── hooks/            # Client-side hooks

The frontend uses Svelte 5 runes ($state, $derived, $effect) for reactivity and shadcn-svelte for the component library. All user-facing strings go through Paraglide-JS for internationalization.

#Docs

Content lives in plain markdown files under docs/, one file per guide. The site that renders them is built separately from @orochibraru/docs and is not part of this repository.

#Database

Penombre uses Drizzle ORM on SQLite by default, with PostgreSQL as an optional alternative for multi-instance deployments. The dialect is picked from the DATABASE_URL scheme; migrations for both live under drizzle/ and are auto-generated via drizzle-kit generate and applied on startup.

#Infrastructure

#Docker

The production Docker image uses a multi-stage build:

  1. base — Alpine + Bun runtime
  2. deps — Install dependencies with a frozen lockfile
  3. builder — Shared source layer both build targets start from
  4. app-builder / docs-builder — Build the app, and the docs site
  5. app / docs — The two runtime images: the app on Bun with ffmpeg and poppler-utils for media thumbnails, the docs site on nginx

The app image runs as a non-root bun user, with a health check built by the SvelteKit adapter.

#Docker Compose

The default compose.yaml defines a single service:

  • app — the Penombre web server on port 3000, with a persistent /data volume holding both the SQLite database and file storage

Optional db (PostgreSQL) and redis services can be added — see Deployment.

#Shared scripts

ScriptPurpose
gen-api.tsGenerate OpenAPI types for the web app from the API spec
circular.tsDetect circular dependencies via madge

This guide lives in the project repo: edit it there, and this page follows within a day.