Briefkasten: a self-hosted bookmarking app rebuilt as a SvelteKit monorepo
📮 Self hosted bookmarking app
At a glance
- What is it?
- Briefkasten is a self-hosted bookmarking service whose version 2 is a complete rewrite in SvelteKit, split into a web app and a Hono API with Prisma and Postgres behind it. The interesting details are the three compose files, the migration baselining step an existing instance needs, and a production image that deliberately leaves the Prisma CLI out.
- Who is it for?
- Briefkasten fits someone who wants their own bookmark server rather than a hosted account, and who is prepared to run Postgres and object storage. It does not fit someone who wants a bookmarking service with no infrastructure to operate, and it does not fit a team that cannot accept SvelteKit and a separate API service.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 32 days ago.
- What is it written in?
- Mainly Svelte, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A rewrite that kept the same URL
Briefkasten is a self-hosted bookmarking app, and the first note in the README is about the version you are about to install.
This is Briefkasten v2, a complete rewrite in SvelteKit, and the previous version, v1, is available on the v1 branch. There is also a migration note for anyone who was using the cloud instance: all of your data has been migrated to the new instance available at the same URL as before.
That last point is worth pausing on, because a rewrite that keeps the URL and the data is a much rarer thing than a rewrite. Most projects start version two with a fresh installation and a migration guide. Here the hosted instance moved house without moving its users, and the v1 code is preserved in the repository rather than deleted, which means a deployment that is not ready for the rewrite can stay on the old branch.
The primary language of the repository is Svelte, with the web front end built on SvelteKit, and the licence is MIT. The project site is briefkastenhq.com and the documentation is at docs.briefkastenhq.com.
The audience is people who want their own bookmark server, with their own data and their own storage, rather than a hosted account that can change its terms. The repository topics list bookmarks, prisma, tailwindcss, planetscale and nextjs, which is a hint at both the storage story and the fact that the project has grown out of another stack.
Two applications, one database, one worker
The setup is described as a monorepo with two parts: apps/web being a SvelteKit web application and apps/backend being a Hono-based API. There are npm scripts in the root package.json to control most things.
That split is the architectural decision. The web app is the interface and the backend is an HTTP API, which means the bookmark data can be reached by something that is not the web app, and the web app can be deployed separately from the API. The root scripts reflect that separation, with dev running both in parallel through filters on sveltekasten-web and sveltekasten-backend, and dev:web and dev:api to run one of them alone.
The database is behind Prisma, and the schema lives with the web app, since the migration path is apps/web/prisma/migrations/0_init/migration.sql and the db:deploy and db:push scripts are filtered to sveltekasten-web.
One small inconsistency is worth knowing before you read the scripts. The repository is briefkasten, the package names and filters use sveltekasten, and the repository field in package.json points at an sveltekasten.git URL. The scripts work either way, but if you are grepping for names, the old name is still in the tooling.
The environment file is where the architecture shows. Both apps need their own .env, and the web one has a PUBLIC_WORKER_URL, which suggests the worker or background processing is reached over the network by the browser-side code rather than in-process.
Login comes first, and it is Better Auth
Before the server will do anything useful, the environment has to be filled in, and the README names the minimum set.
You need at least DATABASE_URL, BETTER_AUTH_SECRET, BETTER_AUTH_URL, PUBLIC_WORKER_URL and one Better Auth authentication provider, for example GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET. The rest of the environment variables depend on the services and features you want to use.
Better Auth is the session and identity layer, and the provider list in the README is the ordinary one: a GitHub OAuth application is the example given, with the credentials coming from the provider's own dashboard. That is a deliberate choice for a self-hosted service. Bookmark servers are, by nature, single-user or small-group installations, and the simplest way to have exactly one account without running a mail server is to borrow an identity you already have.
The consequence is that the deployment has an external dependency the application does not control. The sign-in flow will make a request to the provider, so an instance behind a firewall with no egress to that provider cannot log anyone in, and the client credentials in the environment file become a piece of configuration that has to be kept current.
The setup steps themselves are a short list of commands, and they are in the README as a numbered list with the environment copying in the middle:
$ git clone [email protected]:ndom91/briefkasten.git && cd briefkasten
$ pnpm install
$ cd apps/web && cp .env.example .env
$ cd apps/backend && cp .env.example .envThe note that pnpm install covers both apps is worth internalising, because the monorepo uses a workspace layout and the dependencies for the two applications are installed together.
Fresh installs migrate, existing installs baseline
The database step is the one place where the README's advice changes depending on whether you are starting from nothing, and the distinction is easy to get wrong.
The start command is pnpm db:deploy, which runs prisma migrate deploy against a fresh database. The note underneath explains the other case: pnpm db:deploy runs prisma migrate deploy against a fresh database, so if you are upgrading an existing database whose schema already matches, for example a previous v2 instance, you should baseline it instead with pnpm --filter sveltekasten-web exec prisma migrate resolve --applied 0_init, so the migration is recorded without re-running the DDL. For quick local iteration you can still use pnpm db:push to sync schema.prisma directly.
That is a small paragraph with a real consequence. Running migrate deploy against a populated database can fail, and running migrate resolve against a database that does not have the schema leaves you with a broken app and a migration table that claims everything is applied. The two commands are not interchangeable, and which one is right depends on a fact about your database that the command itself cannot detect.
After that, the three everyday commands are pnpm dev for development, and pnpm build followed by pnpm start for production.
There is also a db:generate script and a db:studio script, the latter filtered to the web app, which is the Prisma Studio for looking at the data.
Three compose files, and storage is the optional one
The Docker story is arranged as three compose files, and the difference between them is what you have to run yourself.
docker-compose.yml is the base: containers for the frontend and backend components of the application. docker-compose.storage.yml adds postgres and rustfs containers for a database and S3-compatible object storage, and you can skip these if you want to use a hosted database provider and an object storage provider instead. docker-compose.local-dev.yml runs the web and backend containers with local code mounted in for development.
The commands are correspondingly short:
docker compose up -d
docker compose -f docker-compose.yml -f docker-compose.storage.yml up -d
docker compose -f docker-compose.local-dev.yml up -dThe base compose file builds two images from one Dockerfile with different targets, briefkasten-web and briefkasten-backend, and each takes its environment from its own env_file. The ports are parameterised, ${PORT:-3000} for the web and ${PORT:-8000} for the backend, and both services sit on a shared briefkasten network, which is how the web app reaches the API by service name rather than by a host address.
The object storage choice is worth a second look. rustfs is an S3-compatible store, which means the application talks to it with the same API it would use for a hosted bucket, and the decision to run it in Docker or point at a provider is a configuration choice rather than a code change. The topics list mentions planetscale, which points the same way, at a hosted database provider as the other half of the arrangement.
The production image skips the Prisma CLI on purpose
The Dockerfile carries comments that explain its own decisions, and they are the most instructive part of the repository.
The base is node:24-bookworm-slim with corepack enabled for pnpm, and the build stage installs openssl and git for Prisma, installs the workspace dependencies with a cache mount, runs prisma generate for the backend and then for the web, and produces deployable production trees with pnpm deploy for each app.
Then the web stage explains what it deliberately does not do. The comment says no prisma generate here, because the build stage already generated the client with engineType = client, Rust-free, and vite bundled it into build/. The production deploy excludes devDependencies, prisma and prisma-zod-generator, so regenerating in this stage would fail with prisma-zod-generator: not found and is unnecessary.
A second comment explains why the container runs node build/index.js directly instead of pnpm start: pnpm would run a deps-status check that reinstalls and triggers the prepare script, which is svelte-kit sync, and that needs devDependencies such as vite and git, which are not in the production image.
Both comments describe the same underlying problem, which is that a SvelteKit application has a build-time half and a runtime half, and the runtime half must not need the build-time half's dependencies. The engineType = client choice removes the Rust query engine from the runtime image, and skipping pnpm removes the last script that would have wanted the dev tooling.
The same care appears in the root package.json, which pins node >= 24 in engines, names pnpm 11.6.0 with a hash in the packageManager field, and uses Biome for formatting, linting and checking rather than a mix of tools.
Editorial conclusion
Briefkasten fits someone who wants their own bookmark server rather than a hosted account, and who is prepared to run Postgres and object storage. It does not fit someone who wants a bookmarking service with no infrastructure to operate, and it does not fit a team that cannot accept SvelteKit and a separate API service. Before you deploy, read the migration baselining note, because an existing instance and a fresh one take different commands, and decide early whether you want the bundled Postgres and S3-compatible storage containers or a hosted provider for both.
Frequently asked questions
How do I set up a self-hosted Briefkasten instance?
Clone the repository, run pnpm install, copy .env.example to .env in both apps/web and apps/backend, and fill in at least DATABASE_URL, BETTER_AUTH_SECRET, BETTER_AUTH_URL, PUBLIC_WORKER_URL and one Better Auth provider such as GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET. Then run pnpm db:deploy once and start with pnpm dev, or pnpm build and pnpm start.
What is the difference between the Briefkasten Docker compose files?
docker-compose.yml runs the web and backend containers, docker-compose.storage.yml adds postgres and rustfs containers for the database and S3-compatible object storage, and docker-compose.local-dev.yml mounts your local code into the containers. You can skip the storage file if you use hosted providers for the database and object storage.
How do I upgrade an existing Briefkasten database?
Do not run pnpm db:deploy against a populated database. If your schema already matches, for example a previous v2 instance, baseline it with pnpm --filter sveltekasten-web exec prisma migrate resolve --applied 0_init so the migration is recorded without re-running the DDL. pnpm db:push is for quick local iteration.
What is new in Briefkasten v2?
v2 is a complete rewrite in SvelteKit, split into a SvelteKit web application in apps/web and a Hono-based API in apps/backend, with Prisma for the database and Better Auth for authentication. The previous v1 version remains available on the v1 branch, and data from the cloud instance was migrated to the new instance at the same URL.
Which licence is Briefkasten released under?
MIT, stated in the README and in the package.json license field, with the LICENSE file at the root of the repository.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/ndom91-briefkasten)Was this useful?