Skip to main content

Self-hosted paste server (sprunge-inspired) with API key auth, soft-delete, and configurable storage

Project description

pste-server

Self-hosted paste server inspired by sprunge. Pastes are world-readable; creating requires an API key.

HTTPS is required in production. API keys appear in the Authorization header and in the /?key=<key> query string used by the web form — both are exposed over plain HTTP. pste-server speaks plain HTTP on port 8000; use a reverse proxy or tunnel to terminate TLS. See examples/Caddyfile and examples/compose-cloudflare.yml.

Quick start

pip install -e .
BASE_URL=https://pste.example.com pste-server

# Add your first API key — prints the bookmark URL directly
pste-admin key add --user alice
# -> https://pste.example.com/?key=AbCd1234...

# Set PSTE_URL on the client to that URL, then:
echo "hello" | pste
# -> https://pste.example.com/AB1234
pste AB1234
# -> hello

See examples/ for Docker Compose, Cloudflare Tunnel, and cloud deployment configurations.

API

GET  /              Help page (add ?key=<key> for the paste web form)
POST /              Create paste (requires Authorization: Bearer <key>)
GET  /<id>          Fetch paste as plain text
GET  /<id>?<lang>   Fetch with Pygments syntax highlighting + copy button

Creating pastes:

Field Type Description
pste string Paste content (required)
lang string Pygments lexer name for syntax highlighting
auto_detect 1 Auto-detect language (Pygments, >0.5 confidence threshold)
single_view 1 Delete after first read
expires_at ISO8601 UTC Absolute expiry timestamp
expires_in_n integer Expiry amount (used with expires_in_unit)
expires_in_unit H/D/W/M Expiry unit: hours, days, weeks, minutes

lang and auto_detect are mutually exclusive — if lang is provided, auto-detection is skipped. expires_at and expires_in_n/expires_in_unit are also mutually exclusive.

Fetching pastes:

  • GET /<id> — always plain text, regardless of stored lang
  • GET /<id>?<lang> — Pygments-highlighted HTML with table line numbers (line numbers have user-select: none so Ctrl-A copies only code) and a Copy button
  • GET /<id>?none — plain text (same as bare GET)

Web form

Open /?key=<key> in a browser to use the paste web form. The key is embedded in the bookmark URL; paste it from pste-admin key add output. The form includes:

  • Textarea for paste content
  • Single-view checkbox
  • Expiry controls (number + H/D/W/M dropdown)
  • Language dropdown (auto-detect default, 27 common lexers)

When submitted with language auto-detect (default), if Pygments identifies the language with >0.5 confidence the result page shows both the plain URL and a highlighted URL. When a specific language is selected the result shows only the highlighted ?<lang> URL.

Managing API keys

# Add a key (prints the full bookmark URL)
pste-admin key add --user alice --notes "personal laptop"

# Specify your own key value (must be [A-Za-z0-9])
pste-admin key add --key MySecretKey --user alice

# List all keys
pste-admin key list

# Revoke by key value (immediate, no confirmation)
pste-admin key revoke --key <key-value>

# Revoke all keys for a user or matching notes (lists keys, requires y to confirm)
pste-admin key revoke --user alice
pste-admin key revoke --notes "old laptop"

# Update key metadata
pste-admin key set --user alice --notes "rotated 2026-07"
pste-admin key set --key <key-value> --disabled true

# List pastes (shows ID, created, lang, key, deleted status)
pste-admin paste list --user alice

Keys take effect immediately — no restart required.

Environment variables

Variable Default Description
BASE_URL http://localhost:8000 Public URL used in paste links and key add output
PORT 8000 Listen port
STORAGE_BACKEND sqlite sqlite, postgresql, or gcs
SQLITE_PATH ./data/pste.db SQLite DB path
DATABASE_URL PostgreSQL connection string
GCS_BUCKET GCS bucket name
MAX_PASTE_BYTES 1048576 Max paste size (bytes)
DARK_MODE false Use github-dark as the highlight style; default (unset) uses default (light)
HIGHLIGHT_STYLE Pin to any Pygments style name, ignoring DARK_MODE

Deletion

By default, expired and single-view pastes are soft-deleteddeleted_at is set and they become inaccessible, but the row is retained. The variables below control hard deletion.

Variable Default Description
DELETE_ON_EXPIRE false Hard-delete immediately on expiry
DELETE_ON_SINGLE_VIEW false Hard-delete immediately on first view
DELETE_AFTER_EXPIRE 7D Hard-delete soft-deleted expired rows after this duration
DELETE_AFTER_SINGLE_VIEW 7D Hard-delete soft-deleted single-view rows after this duration

Duration format for DELETE_AFTER_*: integer + H (hours), D (days), W (weeks), or M (minutes). Set to empty string (DELETE_AFTER_EXPIRE=) to disable deferred hard-deletion entirely. DELETE_AFTER_* runs on a 30-minute cycle, independently of DELETE_ON_*.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pste_server-0.1.0.tar.gz (30.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pste_server-0.1.0-py3-none-any.whl (18.5 kB view details)

Uploaded Python 3

File details

Details for the file pste_server-0.1.0.tar.gz.

File metadata

  • Download URL: pste_server-0.1.0.tar.gz
  • Upload date:
  • Size: 30.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for pste_server-0.1.0.tar.gz
Algorithm Hash digest
SHA256 f9cac38d908fea8565ec50bacb2fe11428995327078c1985d6b4f15fce5fe057
MD5 504511c8deea9a6d57296d64b0a17390
BLAKE2b-256 3929eb076793a06f2a186082b1b8d76538029b79d2587a0d6dfd41c8bbf8be46

See more details on using hashes here.

File details

Details for the file pste_server-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: pste_server-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 18.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for pste_server-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6df769f24db5c3db4e9789a9c1d7c2f9de26ee04c2207d609209d857a13ad268
MD5 1d575c23b028413479c56685fba9792f
BLAKE2b-256 3ab351f8639460612edbdd0fbff8db9582dc99697245f791ce05bb7b2e367471

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page