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
| Layer | Technology |
|---|---|
| Framework | SvelteKit + Svelte 5 |
| Runtime | Bun (via @sveltejs/adapter-bun) |
| Styling | TailwindCSS 4, shadcn-svelte (bits-ui) |
| ORM | Drizzle ORM on SQLite (PostgreSQL optional) |
| Auth | Better Auth (email/password, OAuth, passkeys, API keys) |
| i18n | Paraglide-JS (13 languages, project.inlang) |
| Forms | sveltekit-superforms + Valibot |
| API | OpenAPI-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 hooksThe 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:
- base — Alpine + Bun runtime
- deps — Install dependencies with a frozen lockfile
- builder — Shared source layer both build targets start from
- app-builder / docs-builder — Build the app, and the docs site
- app / docs — The two runtime images: the app on Bun with
ffmpegandpoppler-utilsfor 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/datavolume holding both the SQLite database and file storage
Optional db (PostgreSQL) and redis services can be added — see Deployment.
#Shared scripts
| Script | Purpose |
|---|---|
gen-api.ts | Generate OpenAPI types for the web app from the API spec |
circular.ts | Detect circular dependencies via madge |
This guide lives in the project repo: edit it there, and this page follows within a day.