Quick start

Get from zero to a published page in under two minutes.

  1. Create an account Go to /dashboard, click Sign up, and enter your email and password.
  2. Generate an API key After signing in, open the Account tab and click Generate / Rotate API Key. Copy the key — it is shown only once.
  3. Publish your first page Send a POST request with your HTML content:
curl -s -X POST https://pushpage.link/api/pages \
  -H "X-Api-Key: pp_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"html": "<h1>Hello, pushpage!</h1>", "title": "My First Page"}'

# Response
{
  "url": "https://pushpage.link/pages/abc123.html",
  "id": "abc123"
}

Open the URL in your browser — your page is live. That's it.

Guest publishing: You can call POST /api/pages without any credentials. Guest pages expire after 30 minutes and are not listed in any dashboard.

Authentication

Two credential types are accepted interchangeably on all protected endpoints.

MethodHeaderNotes
API keyX-Api-Key: pp_<key>Long-lived. Ideal for scripts, CI, and agents. Obtain via POST /api/me/tokens.
JWT BearerAuthorization: Bearer <jwt>Short-lived (24 h). Issued by POST /api/auth/login. Used by the dashboard.

API keys

API keys begin with pp_ and are long-lived — they don't expire unless you rotate them. Only one active key per user exists at a time. Rotating a key invalidates the previous one immediately.

Generate or rotate your API key:

curl -s -X POST https://pushpage.link/api/me/tokens \
  -H "Authorization: Bearer <your-jwt>"

# Response
{ "api_key": "pp_abc123..." }
Store your key securely. The raw key is shown exactly once — it is stored only as a SHA-256 hash. If you lose it, rotate to generate a new one.

JWT tokens

JWTs are short-lived (24 h by default) and are the credential used by the browser dashboard. They are issued by logging in with email and password.

curl -s -X POST https://pushpage.link/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"s3cur3pass"}'

# Response
{ "jwt": "eyJ..." }

Admin bootstrap

On first startup with an empty database, pushpage automatically creates an admin account and logs the credentials once to stdout:

==============================================================
No users found — bootstrap admin created.
Email    : [email protected]
Password : <random alphanumeric>
API Key  : pp_abc123...
Copy these credentials now. They will NOT appear again.
==============================================================

Publishing pages

POST /api/pages is the core endpoint. It accepts two content types:

JSON (application/json)

Send an object with an html field and an optional title.

curl -s -X POST https://pushpage.link/api/pages \
  -H "X-Api-Key: $PUSHPAGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"html":"<h1>Report</h1><p>Content here</p>","title":"Q2 Report"}'
FieldTypeRequiredDescription
htmlstringYesFull HTML content. Max size controlled by MAX_FILE_SIZE (default 1 MB).
titlestringNoHuman-readable title. Extracted from the HTML <title> tag if omitted; falls back to "Untitled".

File upload (multipart/form-data)

Upload an .html file directly — no JSON wrapping required. Useful for piping report files from CI or scripts.

curl -s -X POST https://pushpage.link/api/pages \
  -H "X-Api-Key: $PUSHPAGE_API_KEY" \
  -F "[email protected]"

Title resolution order for file uploads:

  1. <title> tag in the HTML content
  2. Original filename without extension (report.html"report")
  3. "Untitled"

Guest publishing

You can call POST /api/pages without any credentials. Guest pages:

  • Expire automatically after 30 minutes
  • Are not tied to any account
  • Do not appear in any user's dashboard
  • Are subject to a lower rate limit (5 requests/minute per IP)
Tip: Guest publishing is great for one-off sharing during development, but API-key authenticated pages last 30 days and are tracked in your dashboard.

Rate limits

Rate limiting applies to POST /api/pages only. All other endpoints are not rate-limited.

User typeLimitScope
Guest (unauthenticated)5 requests/minutePer source IP
Authenticated user10 requests/minutePer user account

When the limit is exceeded, the API returns 429 Too Many Requests. The limits are configurable via environment variables RATE_LIMIT_GUEST_RPM and RATE_LIMIT_USER_RPM.

Dashboard

The browser dashboard is available at /dashboard. It provides a UI for managing your pages and account without writing any code.

Pages tab

Lists all pages you've published, with their URL, creation date, and expiry date. You can delete individual pages.

Account tab

Displays your email and role. You can generate or rotate your API key here. The key is shown once — copy it before closing the dialog.

Users tab (admin only)

Lists all registered users. Admins can promote users to admin or deactivate accounts. Deactivated users retain their pages but can no longer authenticate.

API reference

All endpoints are under /api. The interactive Swagger UI is at /api/swagger-ui/index.html.

Auth

POST /api/auth/signup Public

Create a new account.

# Request
{ "email": "[email protected]", "password": "s3cur3pass" }

# Response: 201 No Content
# Error: 400 (invalid email / password too short), 409 (email taken)
POST /api/auth/login Public

Authenticate and receive a JWT valid for 24 hours.

# Request
{ "email": "[email protected]", "password": "s3cur3pass" }

# Response: 200
{ "jwt": "eyJ..." }
# Error: 401 (invalid credentials or deactivated account)

Pages

POST /api/pages Optional auth

Publish an HTML page. Accepts application/json or multipart/form-data. Returns the page URL and ID.

# Response: 200
{ "url": "https://pushpage.link/pages/abc123.html", "id": "abc123" }

# Error: 400 (missing/empty HTML), 413 (payload too large), 429 (rate limit)
GET /api/pages User

List your pages ordered by creation date descending. Admins see all pages.

# Response: 200 — array of page objects
[{
  "id": "abc123",
  "title": "My Report",
  "url": "https://pushpage.link/pages/abc123.html",
  "created_at": "2026-06-24T10:00:00Z",
  "expires_at": "2026-07-24T10:00:00Z",
  "user_id": "a1b2c3d4"
}]
DELETE /api/pages/{id} User

Delete a page by ID. Users can only delete their own pages; admins can delete any page.

# Response: 204 No Content
# Error: 403 (not your page), 404 (not found)

Account

POST /api/me/tokens User

Generate or rotate your API key. The previous key is invalidated immediately. Returns the raw key once.

# Response: 200
{ "api_key": "pp_abc123..." }

Admin

GET /api/admin/users Admin

List all registered users ordered by creation date.

PATCH /api/admin/users/{id}/deactivate Admin

Deactivate a user. Their pages are retained but they can no longer authenticate.

PATCH /api/admin/users/{id}/promote Admin

Grant admin role to a user.

Health

GET /api/health Public

Returns service health and live statistics (cached for 30 seconds). Returns 200 when healthy, 503 when a critical subsystem is unavailable.

{
  "status": "UP",
  "version": "0.8.0",
  "uptimeSeconds": 3600,
  "livePages": 12,
  "storage": {
    "usedHuman": "200 KB",
    "freeHuman": "10.0 GB"
  }
}

Self-hosting

pushpage is designed to be self-hosted. The entire stack — app, database, and nginx — runs as Docker containers orchestrated by Docker Compose.

Requirements

  • Docker & Docker Compose v2
  • A domain or subdomain pointed at your server
  • A reverse proxy (nginx, Caddy, Traefik) for TLS termination — or use pushpage behind Cloudflare

First-time setup

# 1. Clone the repository
git clone https://github.com/rgf2004/push-page.git
cd push-page

# 2. Create required data directories
mkdir -p data/pages data/pgdata

# 3. Create your .env file (see Configuration section)
cp .env.example .env
nano .env   # set APP_SERVER_URL, JWT_SECRET, etc.

# 4. Start the stack
docker compose up -d

On first start, pushpage automatically creates an admin account and logs the credentials to stdout. Run docker compose logs publisher to see them.

Development (build from source)

docker compose -f docker-compose.dev.yml up -d --build

The dev compose file builds the image from source (no persistent data volumes).

Upgrading

docker compose pull
docker compose up -d

Flyway manages database migrations automatically on startup — no manual schema changes needed.

Configuration

All configuration is via environment variables in a .env file in the project root. Docker Compose passes them to the containers automatically.

Required

VariableDescription
JWT_SECRETSecret for signing JWTs (HS256, min 32 chars). Generate with: openssl rand -base64 32
APP_SERVER_URLFull public URL of your instance, e.g. https://pushpage.example.com. Used to build page URLs in responses.

All variables

VariableDefaultDescription
NGINX_PORT8080Host port nginx binds to.
CLEANUP_RETENTION_DAYS30Days to keep authenticated pages before auto-cleanup.
CLEANUP_SCHEDULE0 0 * * * *Cron expression for the cleanup job (Spring cron format — 6 fields).
MAX_FILE_SIZE1MBMax HTML payload accepted (app-level).
MAX_REQUEST_SIZE10MBServlet-level request ceiling. Should exceed MAX_FILE_SIZE.
RATE_LIMIT_USER_RPM10Max publish requests per minute for authenticated users.
RATE_LIMIT_GUEST_RPM5Max publish requests per minute per IP for guests.
DB_HOSTpostgresPostgreSQL hostname.
DB_PORT5432PostgreSQL port.
DB_NAMEpushpagePostgreSQL database name.
DB_USERpushpagePostgreSQL username.
DB_PASSWORDchangemePostgreSQL password.
Security: Always set a strong, random JWT_SECRET in production. Never use the default DB password.

Generating a JWT secret

# OpenSSL (recommended)
openssl rand -base64 32

# Python
python3 -c "import secrets, base64; print(base64.b64encode(secrets.token_bytes(32)).decode())"

MCP server

pushpage ships a Model Context Protocol server that lets AI assistants like Claude publish and manage pages directly — without shell access or custom tool definitions.

Connecting Claude

Add pushpage to your Claude MCP configuration (~/.claude/mcp.json or via Claude Desktop settings):

{
  "mcpServers": {
    "pushpage": {
      "type": "streamable-http",
      "url": "https://pushpage.link/mcp",
      "headers": {
        "X-Api-Key": "<your-api-key>"
      }
    }
  }
}

If your client does not support the streamable-http transport natively, use mcp-remote as a bridge:

{
  "mcpServers": {
    "pushpage": {
      "command": "npx",
      "args": [
        "mcp-remote",
        "https://pushpage.link/mcp",
        "--header",
        "X-Api-Key:<your-api-key>"
      ]
    }
  }
}

Available tools

ToolDescription
publish_pagePublish HTML content and return the URL.
list_pagesList your published pages.
delete_pageDelete a page by ID.
healthCheck service health and stats.

Once connected, Claude can call these tools naturally. For example: "Publish this HTML report and give me the link" — Claude will call publish_page and return the URL inline.

Self-hosted MCP: If you're running your own instance, point the url at your own domain: https://your-domain.com/mcp.