Co-authored-by: Cursor <cursoragent@cursor.com>
8.5 KiB
MincedPad Design Spec
Date: 2026-07-14
Project: MincedPad — 自托管公共数据共享与记事本平台
Status: Draft for review
1. Overview
MincedPad is a lightweight, self-hosted web tool for anonymously pasting text and uploading files, then sharing via a public wall and/or a direct link. No registration is required for visitors. An admin password gate provides moderation (delete content, ban IPs).
Goals
- Zero-login create / share / view
- Public wall by default, optional link-only
- Markdown text + common file upload/preview/download
- TTL (default 24h) and burn-after-read
- Docker Compose one-shot deploy, low resource use
- Rate limit, upload size cap, admin moderation
Non-goals (v1)
- Multi-admin RBAC / audit export
- Multi-node / object-storage backends
- Real-time WebSocket wall updates
- End-to-end encryption (content is readable by the server)
2. Decisions (confirmed)
| Topic | Choice |
|---|---|
| Name | MincedPad |
| Stack | FastAPI + Vue3 + Element Plus SPA |
| Architecture | Monorepo; frontend built into backend static hosting; one container |
| Storage | SQLite metadata + local data/uploads/ files |
| Visibility | Default public on wall; optional “link only” |
| Expiry | Default 24h; options 1h / 24h / 7d / never; optional burn-after-read |
| Admin | Single password via env → JWT |
| Rate limit | 2 create/upload requests per IP per second |
| Max file | 200 MB (env configurable) |
3. Architecture
Browser (Vue3 + Element Plus)
│
▼
FastAPI (single process)
├── /api/* JSON API
├── /admin UI routes (SPA)
├── /* SPA static (dist/)
├── SQLite data/mincedpad.db
└── Files data/uploads/<random>
└── Cleaner thread expire + burn cleanup
Repository layout
MincedPad/
├── backend/
│ ├── app/
│ │ ├── main.py
│ │ ├── config.py
│ │ ├── db.py
│ │ ├── models.py
│ │ ├── schemas.py
│ │ ├── api/
│ │ │ ├── public.py
│ │ │ └── admin.py
│ │ ├── services/
│ │ │ ├── items.py
│ │ │ ├── storage.py
│ │ │ ├── rate_limit.py
│ │ │ ├── bans.py
│ │ │ └── cleaner.py
│ │ └── security.py
│ ├── requirements.txt
│ └── Dockerfile
├── frontend/
│ ├── package.json
│ ├── vite.config.ts
│ └── src/
│ ├── views/ # Home, ItemDetail, Admin
│ ├── api/
│ └── components/
├── data/ # runtime volume (gitignored)
├── docker-compose.yml
├── README.md
└── docs/superpowers/specs/
Runtime
- Default listen
:8080 - Compose mounts
./data:/data - Env:
ADMIN_PASSWORD,SECRET_KEY,MAX_UPLOAD_MB=200,RATE_LIMIT_PER_SECOND=2
4. Data model
items
| Column | Type | Notes |
|---|---|---|
| id | INTEGER PK | |
| slug | TEXT UNIQUE | short public id (e.g. nanoid) |
| kind | TEXT | text | file |
| title | TEXT | optional; default from first line or filename |
| body | TEXT NULL | markdown/plain for text items |
| file_name | TEXT NULL | original name |
| file_path | TEXT NULL | relative path under uploads |
| mime | TEXT NULL | |
| size_bytes | INTEGER NULL | |
| is_public | BOOLEAN | true = on wall; false = link only |
| burn_after_read | BOOLEAN | |
| expires_at | DATETIME NULL | null = never |
| created_ip | TEXT | |
| created_at | DATETIME | |
| view_count | INTEGER | |
| burned | BOOLEAN | true after burn consumed |
banned_ips
| Column | Type | Notes |
|---|---|---|
| ip | TEXT PK | |
| reason | TEXT | |
| created_at | DATETIME |
Admin auth uses signed JWT (HS256) with SECRET_KEY; no separate sessions table in v1.
5. API
Public
| Method | Path | Description |
|---|---|---|
| POST | /api/items |
Create text item (JSON) |
| POST | /api/items/upload |
Create file item (multipart) |
| GET | /api/items/wall |
Paginated public wall (page, page_size) |
| GET | /api/items/{slug} |
Get item metadata + text body; may burn |
| GET | /api/items/{slug}/file |
Download/inline file |
Create body (text) fields: body, title?, is_public (default true), burn_after_read (default false), ttl (1h|24h|7d|never, default 24h).
Upload form fields: file + same options.
Errors: 413 too large, 429 rate limited, 403 banned IP (writes), 404 missing/expired/burned.
Admin (Bearer JWT)
| Method | Path | Description |
|---|---|---|
| POST | /api/admin/login |
{ password } → { token } |
| GET | /api/admin/items |
All items, newest first |
| DELETE | /api/admin/items/{id} |
Force delete + file |
| GET | /api/admin/bans |
List bans |
| POST | /api/admin/bans |
{ ip, reason? } |
| DELETE | /api/admin/bans/{ip} |
Unban |
6. Frontend
Pages
-
Home
/- Composer: Markdown textarea (Element Plus) + drag-and-drop upload zone
- Options: TTL select (default 24h), burn-after-read switch, link-only switch
- Actions: “发布文本” / 上传后自动生成结果
- Result: copyable share URL
- Wall: recent public items (poll every ~15s or on focus)
-
Detail
/p/:slug- Text: Markdown render
- Image MIME: inline preview
- Other files: download button (+ optional content-disposition inline for pdf when safe)
- Burn warning before first view if applicable
-
Admin
/admin- Password login
- Table of items with delete
- Ban IP form + ban list with unban
UX constraints
- Anonymous-first: landing page is immediately editable
- Clear toasts for size/rate/ban failures
- Friendly 404 for expired/burned content
- Mobile-first responsive: usable on phones (portrait); touch-friendly controls; composer / wall stack vertically on small screens; file pick works via mobile OS picker; no hover-only actions; Element Plus grid/breakpoints (
xs/sm) for layout
Mobile acceptance
- Home / Detail / Admin usable at ~375px width without horizontal scroll of the main layout
- Create text, upload file, copy share link, open wall item on a phone browser
- Admin login and delete/ban actions work on touch devices
7. Security & limits
- Upload cap: default 200 MB via
MAX_UPLOAD_MB - Rate limit: 2 create/upload ops per IP per second (in-memory sliding window; acceptable for single-node)
- Ban list: banned IPs cannot create/upload; wall and read still allowed unless later tightened
- Filename: store as random UUID + safe extension; never trust client path
- Extension allowlist (v1): common images (
png/jpg/jpeg/gif/webp/svg), text/docs (txt/md/pdf/doc/docx/xls/xlsx/ppt/pptx/csv), archives (zip/7z/tar/gz/rar), plusjson - Admin:
ADMIN_PASSWORDrequired at startup in production; JWT expiry 12h
8. Background cleanup
A lightweight loop (async task or thread) every 60s:
- Delete items where
expires_at < now - Delete burned items already marked and past grace (immediate delete on burn read is OK)
- Remove orphan files if item row gone
9. Docker / deploy
# conceptual
services:
mincedpad:
build: .
ports: ["8080:8080"]
environment:
ADMIN_PASSWORD: "change-me"
SECRET_KEY: "change-me"
MAX_UPLOAD_MB: "200"
RATE_LIMIT_PER_SECOND: "2"
volumes:
- ./data:/data
restart: unless-stopped
Multi-stage Dockerfile: build frontend → copy dist into backend image → run uvicorn.
10. Acceptance criteria (v1)
- Unauthenticated users can create text and upload files and receive a share URL.
- New items are on the public wall unless “link only” is set.
- Default TTL is 24h; 1h / 7d / never and burn-after-read work.
- Markdown renders; images preview; other allowed types download.
- Admin can log in with env password, delete items, ban/unban IPs.
- Uploads over 200 MB rejected; more than 2 create/upload requests per second per IP get 429.
docker compose up -dbrings up a working instance on port 8080.- Phone browsers (~375px) can create, upload, view, copy links, and use admin actions without broken layout.
11. Implementation order (for planning)
- Backend skeleton: config, DB models, create/get/wall, file storage
- Rate limit + bans + cleaner
- Admin auth + moderation APIs
- Vue pages: Home, Detail, Admin
- Docker multi-stage + compose + README
- Smoke tests / manual acceptance checklist