Blandcamp
Git Music Server
Introducing...
Blandcamp is a music store that you run yourself. A release arrives as a git push, and the store streams the tracks and sells the downloads.
Payments land in your own Stripe account, and all data is kept on a machine that only you own. One binary and one data dir is the whole install, and the HTML, CSS and JS are yours to edit.
Just like Wargames, the control panel is a terminal you connect to over SSH.
It can run on pretty much anything... the old family computer you haven't turned on in years, the laptop with the broken screen you still keep around, or a Pi Zero as a dedicated device.

- Stripe checkout
- 134 currencies
- Name-your-price
- Secure downloads
- Subscriber emails
- Discount codes
- Public/private releases
- Bitrate controls (down to 16kbps)
- Analytics/CSV export
- 47 themes (or byo)
- ActivityPub
- One admin, many artists
- Custom domain
- Syntax highlighting
- Customisable templates
$35
Linux/Mac/Pi
Run on Pi

Be your own datacentre.
You are the only shareholder, there are no board meetings.
Blandcamp can be run on many types of Raspberry Pi, from Pi Zero to Pi 5. No monitor, no keyboard — connect over wifi or Ethernet. We recommened the Pi 5 or Pi Zero 2W.
Blandcamp supports the Waveshare e-ink hat. The display cycles through stats such as stream counts, sales data, country popularity. Simplest install to get running, feel free to overcomplicate. Flash an SD card with Pi Lite OS, whichever version your Pi supports, and enable SSH. The instructions below assume the user you created was called By default, blandcamp listens on port Install on Pi
diana and the device was called blandcamp. The scp line takes the arm64 build, so swap in the file for your Pi.# copy the build to your Pi
scp blandcamp-linux-arm64.tar.gz [email protected]:
# after sshing in
sudo apt-get update
sudo apt-get install -y ffmpeg
# unpack it and run the TUI
tar xzf blandcamp-linux-arm64.tar.gz && cd blandcamp
./blandcamp
:2468, so on your own network it answers at http://blandcamp.local:2468. The reverse proxy or the router comes next; set either up carefully.Pi builds
Pi image download file Pi Zero, Zero W, Pi 1 32-bit Linux (ARMv6 / Pi Zero) blandcamp-linux-arm6Pi 2, Pi 3, Pi 4 32-bit Linux (ARM / Raspberry Pi 2+) blandcamp-linux-armZero 2 W, Pi 3, Pi 4, Pi 5 64-bit Linux (ARM64 / Raspberry Pi) blandcamp-linux-arm64

Case for Pi Zero 2W with e-ink hat.
Pi will snap fit! No supports required.

Docs
Commands
blandcamp the dashboard
blandcamp serve the web store and git transport, in the foreground
blandcamp templates write the default templates to <data>/templates
blandcamp create create a release and print its push instructions
blandcamp artist create a non-admin artist
--data DIR data dir (default: $BLANDCAMP_DATA, else ./data)Requirements
- A server or computer with SSH: an old desktop, a Pi 5, a VPS
- Git and ffmpeg installed, to push releases and make the stream
- A custom domain
- A Stripe account, to sell downloads
- Enough git knowledge to push a repo
Interface
The Blandcamp site is read-only, and everything on it comes from a file you can edit.
Templates
One file per page. layout.html is the frame around them: the head, the header, the stylesheet links. Each page says what goes in the middle.
- index.html
- The front page, a grid of every published release and the server's readme.
- repo.html
- One release. The cover, the player, the readme, the files, the commits, the replies.
- file.html
- One source file, highlighted.
- cart.html
- The basket, with a discount code field and the mailing list opt-in.
- receipt.html
- An order and its download links.
- list.html
- The page a mailing list confirmation or unsubscribe link lands on.
- profile.html
- The fediverse profile: a handle, the counts, the releases.
- share.html
- The embed a release is posted as. Its own document, no layout.
Fonts, stylesheets and modules sit in a public/ dir beside the pages, served from /public/. A module is imported by name, so adding one means adding that name to layout.html.
There are 47 themes from the hundredrabbits collection, and an svg dropped in themes/ under the data dir joins them. Pick one in the TUI and every open tab repaints where it stands.
Your own files
Three files in the data dir are yours to drop in.
- profile.png, .jpg or .gif
- The account's picture, served to the fediverse. One image covers the store and its artists.
- favicon.png, .svg or .ico
- The tab icon.
- readme.md
- The store's own readme, under the releases on the front page.
Without a picture or an icon, the store draws its own mark.
Editing the templates
The default templates are built in. To take them over, run the binary against your data dir:
blandcamp templates
That writes the templates and the assets into <data>/templates, and the store uses that copy from then on. Edit a file and reload the page to see it. Restart blandcamp serve once, after the first export.
blandcamp templates overwrites whatever is there, so keep your own edits somewhere else as well. New sheets go in public/css, new modules in public/js inside it, linked from layout.html.
The palette is a block of CSS variables at the top of layout.html, and everything else is an ordinary file. Nothing is compiled between an edit and the page.
Terminal
Run blandcamp over SSH and you are in the dashboard. There is no web admin. esc leaves a screen; esc on the dashboard quits.
Every list works the same: up and down move, enter selects, esc goes back. Typing jumps to the first entry with that prefix, and each menu remembers where the cursor was.
Setup
On a fresh store the dashboard opens on Create Admin User instead of the menu. The admin is the way in, and cannot be deleted later.
- Admin name
- Your name. The slug is proposed from it: enter accepts, or type another.
- Password
- You type it twice and never see it.
- Server Name
- The store's name on the site.
- Currency
- The currency for prices, from Stripe's list.
Blandcamp
The dashboard. The header shows the site URL, and, with federation on, the store's handle and follower count.
- Create Release
- Start a new release.
- Edit Release
- Pick one you already made. Hidden until there is one.
- Sales: N
- The purchases. Hidden until there is one.
- Subscribers: N
- The mailing list. Hidden until there is one.
- Artists: N
- The artists that may push. The admin is not counted.
- Comments: N
- Comments on every release, and how many are new. Always in the list, 0 included.
- Settings
- The server, payments, mail, the fediverse, and the store-wide choices.
esc quits.
Create release
Asks for a title, then shows the slug and takes enter or a new one. That makes the release's git repo, prints its push instructions, and waits.
git remote add origin https://your.store/push/<slug>
git push origin main
A fresh repo gets the git init lines too. The wait counts seconds, and esc skips it. When a push lands it prints the tracks read from their tags, then the file tree, and drops into the release editor.
Release editor
One release, headed title (slug).
- Title: X
- The name on the page. The slug stays: it is the URL and the git remote.
- Artists: none
- Who may push to it. The artist name the page shows comes from the same artists.
- Cover: auto
- Auto uses a
cover.*file from the release. Pick an image to override it. - Published: yes/no
- Publishing also announces it to followers, once per release.
- Tracks: N Locked
- Locks tracks, counting them beside the label once one is. A locked one keeps its place in the player and does not play.
- Price: Free
- Set it on the price screen.
- Streaming: default (192 kbps)
- The rate this release streams at, without moving the store-wide one. Default rides it.
- Password: none
- A shared password to read it. Empty clears it.
- Plays: N
- Plays and listeners per track, over the last two weeks.
- Comments: N
- Comments on this release, hidden ones included.
- Codes: N · N used
- Discount codes for this release, spent or waiting.
- Theme: default (Autism)
- A palette for this release's page alone. Default rides the store's.
- Preview
- A 24h link to show it before publishing. Remake it to revoke. In the list while the release is unpublished.
- Delete
- Deletes the release and takes down the post about it.
Price
Name Your Price or Fixed, then the amount in whole cents. A floor may be zero; a fixed price may not.
Tracks
Every track with its state beside it, [locked] on the ones that are, and Unlock All or Lock All above. Picking one flips it.
Locking keeps a track out of the stream and leaves it in the player, so a couple can be heard before anyone buys and the rest are kept. The download carries every track either way.
Codes
A code takes money off one release, or covers it whole, and each one works once. Generate asks what they are worth, how many, and when they lapse. List Unspent appears once there is one, and prints the ones nobody has used.
Buyers type one into the cart, under the total.
Artists
The artists that may push to the git server. The header names the admin, which is not in the list.
- Add Artist
- A login for a new artist, who starts with no releases.
- an artist
- Give it a title, set push access, reset its password, or delete it.
Deleting one leaves you on the list, not on the artist that is gone.
An artist has no picture of its own: the store's stands for all of them.
Push access
The catalogue with this artist's access beside each release, as slug: on or slug: off. Selecting one toggles it. The same list is reachable from a release's own menu, under Artists.
Comments
Every comment the store holds, newest first, or one release's when opened from its menu. A comment that arrived since you last looked is prefixed *.
- Mark All as Read (N new)
- Clears the unread marks.
- a comment
- The handle and a preview of the comment.
One comment opens on who wrote it and the whole text, then offers Reply, Hide (or Show), Block <host>, and Delete.
Sales
The header is the purchase count and the revenue. A row is date, amount, release and email, with a free download marked free.
- Export CSV
- Every sale to a CSV in
data/exports. - a sale
- One sale and the link to hand over if the buyer lost it.
The link carries the buyer's token, so the export is a credential: keep it off the web.
Subscribers
The header is who is on the list and how many are waiting to confirm. A row is the day, the state, and the address.
- Export CSV
- Every address with its state and its dates, to a CSV in
data/exports. - an address
- One address and where it stands: on the list, waiting, or gone.
The list is seen and exported here alone: the store the public reads has no admin routes to hang it on.
Settings
- Server
- The name, the readme, the domain, the proxy, the port, your password.
- Payments
- The currency and the Stripe keys.
- The SMTP the store sends from, and the mail to the list.
- Fediverse
- The store's handle, its profile, its followers.
- Push to Create: on/off
- Whether a push to a slug nobody owns makes a release. Off, releases are made here, and an artist pushes only to what they were granted.
- Bitrate: N kbps
- The stream quality: 16 to 320 kbps, 192 unless you say otherwise.
- Theme: X
- The palettes on offer, the built-in ones and your own. Moving down the list previews each one.
Server
- Server Name
- The store's name on the site.
- Server Readme
- The store's own readme, from readme.md in the data dir, shown on the front page.
- Domain
- The public hostname, with no https:// and no port. Local names get plain HTTP.
- Proxy
- The addresses a proxy comes from, or off. Only a peer in that list may name the client address; Cloudflare is recognised by its own addresses and needs no entry.
- Port
- The port it listens on. A running server keeps the old one until restarted.
- Password
- Change your password.
- Release Notice: on/off
- The store asks whether there is a newer build, once a day, and the header carries it as
v1.4 -> v1.5when there is. Nothing but the version moves. Check Now asks on the spot.
Payments
The store is the payee: every payment goes to the admin's Stripe, and the checkout never asks who made a release. Run it as a label and you settle with your artists yourself: every row names its release, and the export is the ledger to add up from.
The webhook URL is printed to paste into Stripe's dashboard. Each key reads set or not set and is never shown: paste a new one, or enter to keep.
- Currency
- The currency for prices and checkout.
- Secret Key
- From Stripe's API keys page. A restricted one is enough.
- Publishable Key
- Unused: buyers land on Stripe's own page, so it can stay empty.
- Webhook Secret
- From the webhook you point here, added by hand under Developers, then Webhooks. Subscribe it to
checkout.session.completed, and tocheckout.session.async_payment_succeededif you want the slow methods covered. Anything else is ignored. Without it, an order is only safe if the buyer stays on the page.
The secret key can be a restricted key. The store makes three calls, all on Checkout Sessions: create one when a buyer checks out, then read the session and its line items when the order lands. Nothing else is asked of Stripe, so Checkout Sessions write is the whole permission.
Where the store sends from. A buyer gets their download link by mail when an order lands, and publishing a release mails the list. The header carries the subscriber count.
- Host / Port / Username / Password
- Your SMTP account. Port 587 unless yours differs.
- From
- The address the mail comes from.
- Send Test
- Renders the purchase mail for an address you name, then sends it.
- Email About a Release...
- The release mail to the list, for one that went out before mail was set up.
One plain text file per message, in mail/: confirm.txt, purchase.txt, release.txt. blandcamp templates writes them out, so they are yours to edit.
The list is built from two opt-ins: the cart, and the free download form. An address that arrives any other way is mailed a confirmation first, and joins on the click. Every mail carries its own unsubscribe link, so it goes out one address at a time.
Fediverse
The server posts from one handle, and every artist posts from their own. Follow the server from Mastodon and the whole catalogue arrives; follow an artist and only their releases do. An artist's handle comes from their slug, so giving them a title does not move it.
A release page prints the handle to follow for that release: the artist holding it, or the server when nobody does. Replies land on whoever posted it.
The header carries the handle, and the delivery queue when it has anything in it.
- State: off
- Turns it on. Warns first if you have no public domain.
- Handle
- Your handle. Renaming it loses your followers, so it asks first.
- Bio: auto
- The text on your profile. Basic HTML.
- Replies: on
- Accept replies to releases.
- Followers: N
- Who follows. Pick one to remove it.
- Blocked: N
- Domains you blocked. Pick one to unblock.
Preparing a release
Make flacs from your source and keep them in one folder. A cover.png or cover.jpg beside them becomes the release's cover.
Tag the tracks before you push. The tags are the track list: title, artist and album are read from the files, and the player's order is the track number, so untagged files land at the end. Kid3 is free and fills a folder in one pass. Any tagger does.
The repo carries more than audio. A text file in it opens on its own page, highlighted, so a readme or the source of a patch sits beside the tracks. A binary is listed and not printed.
What you push is what buyers get: the stream is an mp3 made at the store's bitrate, and the download is the folder exactly as you pushed it, names and tree and all.
Hosting
The store is one binary and one data dir. How you run them is up to you.
- The data dir is the whole store: bland.db, the release repos, the art and audio it makes, the themes and templates you add. Copy that dir and the backup is done.
- FFmpeg makes each stream on demand. Without it the downloads still work and the player has nothing to play.
- The suggested shape is a reverse proxy in front, with Caddy doing the TLS and its renewals. Name it in Settings, or every play and free download reads as one visitor.
- A Cloudflare tunnel gets the store online from a machine with no public address, and nothing is forwarded. It is a proxy like any other, so name what it arrives from in Settings.
- A static IP from your ISP is the other way to be reachable, and the reverse proxy still sits in front. A home address that moves takes the name in Domain with it.
- Nothing here needs a bot filter. If the crawlers ever bother you, Anubis or go-away can sit in front, and the Stripe webhook, the fediverse and git have to stay out of the challenge: a server cannot answer one.
- Set the domain before you publish. The fediverse handle, the mail links and the push instructions read from it.
Support
Setup questions and bug reports go to irc.auti.sm.
Mail works too: [email protected]. So does Mastodon.
Bought it and lost the link? Email us.
License
Blandcamp is nonfree software. The licence covers one instance. The binary is yours to run, not to pass on or resell. We accept no responsibility for your self-hosting setup.
It comes as it is, with no warranty. Questions get answered in the irc channel.
It's now safe to turn off your computer.