4f8c5b0c638ef0d7295383822b0e7711b0ec002e
SoftDelete now reports whether it performed the delete (conditional UPDATE ... WHERE deleted_at IS NULL via RowsAffected). RegisterRead returns an admitted flag: legacy burn pastes admit exactly one reader (atomic soft-delete win), and burn-after-N pastes increment reads_used via a conditional UPDATE guarded on reads_used < reads_limit, so concurrent readers cannot both consume the final read. API/HTML/raw read paths return 404 when the reader loses the burn claim. Adds concurrency regression tests: 24 parallel readers of a burn paste (exactly one receives content) and 30 parallel readers vs a 3-read budget (exactly 3 admitted).
Palette
Palette is a fast, self-hosted pastebin. One Go binary, a SQLite database, and a web UI for sharing code and text with links that expire on your terms.
Features
- Paste cans — bundle notes, text, and files into one shareable page
- Password lock — protect individual pastes with a password
- Custom expiry — from 1 minute up to 1 year, or never
- Burn after N reads — a paste that vanishes after a chosen number of reads
- Custom URLs — reserve
/my-snippetinstead of a random slug - Syntax highlighting with language auto-detection (go-enry)
- Rate limiting on create and unlock
- Saved page — see and manage everything created from your browser
- API-first — every UI action is also a plain HTTP call
- Single binary — templates and assets are embedded; no external deps
Get Started
Build from source
Requires Go 1.21+.
go build -o palette .
./palette
# open http://localhost:8080
Docker
docker run -p 8080:8080 -v palette-data:/data git.archfox.org/poslop/palette
The SQLite database lives in the /data volume inside the container.
Screenshots
![]() |
![]() |
![]() |
Configuration
| Setting | Default | Description |
|---|---|---|
PALETTE_ADDR |
:8080 |
Listen address |
PALETTE_DB |
palette.db |
SQLite database path |
PALETTE_MAX_TEXT |
5242880 |
Max paste size in bytes (5 MB) |
PALETTE_MAX_ITEM |
26214400 |
Max can item size in bytes (25 MB) |
PALETTE_ADMIN_KEY |
generated | Admin key; if unset a 32-char hex key is generated and persisted to <db-dir>/admin-key (0600) |
Admin
GET /admin serves the admin page. Enter the admin key there — it is stored in
sessionStorage (never a cookie) and sent as the X-Admin-Key header on
GET/POST /admin/api/settings.
The admin API reads/sets: rate-limit burst, rate-limit refill per minute, max content bytes, default expiry, custom URL reservation days, and the burn viewer window (minutes). All admin access attempts are logged.
./palette --reset-admin-key # regenerate the admin key and print it
API
Create a paste with one call:
curl -X POST http://localhost:8080/api/pastes \
-H "Content-Type: application/json" \
-d '{"content": "print(hello)", "language": "python", "expires_in": "168h"}'
Full API docs: docs/API.md.
CI
Gitea Actions workflow at .gitea/workflows/ci.yml:
- On push to main:
go vet+go test - On tags: build and push Docker image to
git.archfox.org/poslop/palette


