Quick start
Get from zero to a published page in under two minutes.
- Create an account Go to /dashboard, click Sign up, and enter your email and password.
- 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.
- 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.
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.
| Method | Header | Notes |
|---|---|---|
| API key | X-Api-Key: pp_<key> | Long-lived. Ideal for scripts, CI, and agents. Obtain via POST /api/me/tokens. |
| JWT Bearer | Authorization: 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..." }
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"}'
| Field | Type | Required | Description |
|---|---|---|---|
| html | string | Yes | Full HTML content. Max size controlled by MAX_FILE_SIZE (default 1 MB). |
| title | string | No | Human-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:
<title>tag in the HTML content- Original filename without extension (
report.html→"report") "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)
Rate limits
Rate limiting applies to POST /api/pages only. All other endpoints are not rate-limited.
| User type | Limit | Scope |
|---|---|---|
| Guest (unauthenticated) | 5 requests/minute | Per source IP |
| Authenticated user | 10 requests/minute | Per 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
Create a new account.
# Request
{ "email": "[email protected]", "password": "s3cur3pass" }
# Response: 201 No Content
# Error: 400 (invalid email / password too short), 409 (email taken)
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
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)
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 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
Generate or rotate your API key. The previous key is invalidated immediately. Returns the raw key once.
# Response: 200
{ "api_key": "pp_abc123..." }
Admin
List all registered users ordered by creation date.
Deactivate a user. Their pages are retained but they can no longer authenticate.
Grant admin role to a user.
Health
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
| Variable | Description |
|---|---|
| JWT_SECRET | Secret for signing JWTs (HS256, min 32 chars). Generate with: openssl rand -base64 32 |
| APP_SERVER_URL | Full public URL of your instance, e.g. https://pushpage.example.com. Used to build page URLs in responses. |
All variables
| Variable | Default | Description |
|---|---|---|
| NGINX_PORT | 8080 | Host port nginx binds to. |
| CLEANUP_RETENTION_DAYS | 30 | Days to keep authenticated pages before auto-cleanup. |
| CLEANUP_SCHEDULE | 0 0 * * * * | Cron expression for the cleanup job (Spring cron format — 6 fields). |
| MAX_FILE_SIZE | 1MB | Max HTML payload accepted (app-level). |
| MAX_REQUEST_SIZE | 10MB | Servlet-level request ceiling. Should exceed MAX_FILE_SIZE. |
| RATE_LIMIT_USER_RPM | 10 | Max publish requests per minute for authenticated users. |
| RATE_LIMIT_GUEST_RPM | 5 | Max publish requests per minute per IP for guests. |
| DB_HOST | postgres | PostgreSQL hostname. |
| DB_PORT | 5432 | PostgreSQL port. |
| DB_NAME | pushpage | PostgreSQL database name. |
| DB_USER | pushpage | PostgreSQL username. |
| DB_PASSWORD | changeme | PostgreSQL password. |
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
| Tool | Description |
|---|---|
| publish_page | Publish HTML content and return the URL. |
| list_pages | List your published pages. |
| delete_page | Delete a page by ID. |
| health | Check 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.
url at your own domain: https://your-domain.com/mcp.