Files
MincedPad/docs/superpowers/specs/2026-07-14-mincedpad-design.md
T
2026-07-14 12:21:10 +08:00

7.9 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

  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

# 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