# 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/ └── 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 1. **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) 2. **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 3. **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 ## 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`), plus `json` - **Admin:** `ADMIN_PASSWORD` required at startup in production; JWT expiry 12h ## 8. Background cleanup A lightweight loop (async task or thread) every 60s: 1. Delete items where `expires_at < now` 2. Delete burned items already marked and past grace (immediate delete on burn read is OK) 3. Remove orphan files if item row gone ## 9. Docker / deploy ```yaml # 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) 1. Unauthenticated users can create text and upload files and receive a share URL. 2. New items are on the public wall unless “link only” is set. 3. Default TTL is 24h; 1h / 7d / never and burn-after-read work. 4. Markdown renders; images preview; other allowed types download. 5. Admin can log in with env password, delete items, ban/unban IPs. 6. Uploads over 200 MB rejected; more than 2 create/upload requests per second per IP get 429. 7. `docker compose up -d` brings up a working instance on port 8080. ## 11. Implementation order (for planning) 1. Backend skeleton: config, DB models, create/get/wall, file storage 2. Rate limit + bans + cleaner 3. Admin auth + moderation APIs 4. Vue pages: Home, Detail, Admin 5. Docker multi-stage + compose + README 6. Smoke tests / manual acceptance checklist