Project architecture
Overview of the Penombre project architecture
Overview
Penombre is structured as a Bun monorepo with three packages:
Web
SvelteKit full-stack app (frontend + REST API).
Mobile
Expo + React Native cross-platform app.
Docs
Next.js + Fumadocs documentation site.
penombre/
├── packages/
│ ├── web/ # SvelteKit — frontend + REST API + database
│ ├── mobile/ # Expo — React Native client
│ └── docs/ # Next.js — documentation
├── scripts/ # Shared tooling (API codegen, DB diagram, circular checks)
├── Dockerfile # Multi-stage production build
└── compose.yaml # Docker Compose (app + PostgreSQL)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 svelte-adapter-bun) |
| Styling | TailwindCSS 4, shadcn-svelte (bits-ui) |
| ORM | Drizzle ORM on PostgreSQL |
| Auth | Better Auth (email/password, OAuth, passkeys, API keys) |
| i18n | Paraglide-JS (English + French) |
| 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 helpersOpenAPI-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
- Expo — mobile app deep-link authentication
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.
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 (en/fr)
└── 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.
Mobile package
An Expo + React Native app that connects to a Penombre instance.
Tech stack
| Layer | Technology |
|---|---|
| Runtime | Expo 55, React Native 0.81 |
| Routing | Expo Router (file-based) |
| Styling | NativeWind (TailwindCSS for RN) |
| Data fetching | SWR |
| API client | openapi-fetch (shared types with web) |
| Auth | Better Auth Expo plugin + SecureStore |
The mobile app authenticates via deep links (penombre:// scheme) and stores credentials in encrypted device storage using Expo SecureStore. API types are shared with the web package through the OpenAPI code generation pipeline.
Docs package
A static documentation site built with Next.js 16 and Fumadocs, exported as static HTML for deployment anywhere.
Content lives in MDX files under content/docs/. Full-text search is powered by Orama.
Database
Penombre uses PostgreSQL with Drizzle ORM. Migrations are auto-generated via drizzle-kit generate and applied on startup.
Run bun run db:diagram from the project root to regenerate this diagram.
Infrastructure
Docker
The production Docker image uses a multi-stage build:
- base — Alpine + Bun runtime
- builder — Install dependencies with frozen lockfile
- frontend-builder — Build the SvelteKit app with Vite
- final — Minimal runtime with
ffmpegandpoppler-utilsfor media thumbnails
The final image runs as a non-root bun user with a health check on /api/health.
Docker Compose
The default compose.yaml defines two services:
- app — the Penombre web server on port
3000, with a persistent/datavolume for file storage - db — PostgreSQL 17 Alpine with a health check and persistent data volume
Shared scripts
| Script | Purpose |
|---|---|
gen-api.ts | Generate OpenAPI types for web and mobile from the API spec |
db-diagram.ts | Generate the database diagram (DBML → SVG) |
circular.ts | Detect circular dependencies via madge |
backup.sh | Back up the PostgreSQL database |
restore.sh | Restore a PostgreSQL database backup |
Found an issue or want to contribute? Edit this page on GitHub