System-level overview of Tanabata File Manager (TFM). For the product-level
requirements see REQUIREMENTS.md; for per-side detail see
GO_PROJECT_STRUCTURE.md (backend) and
FRONTEND_STRUCTURE.md (frontend). The full HTTP contract
lives in openapi.yaml.
TFM is a multi-user, tag-based web file manager for images and video. It is a single deployable unit β one Docker image that serves both the REST API and the built single-page app on one port β plus a PostgreSQL database.
βββββββββββββββββββββββββββββββββββββββββββββ
Browser / installed β Reverse proxy (nginx, TLS) β
PWA (desktop/mobile) β host: 443 β 127.0.0.1:${APP_PORT} β
β HTTPS βββββββββββββββββββββββ¬ββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββΌβββββββββββββββΊ 127.0.0.1:42776
β
βββββββββββββββββββββββββββΌβββββββββββββββββββββββββ
β Tanabata container (single image) β
β β
β Go server (Gin) β
β ββ /api/v1/* REST API β
β ββ /health liveness β
β ββ /* static SPA + index.html β
β fallback β
β β
β Disk: /data/files (originals, name = UUID) β
β /data/thumbs (thumbnail/preview cache) β
β /data/import (server-side import drop) β
βββββββββββββββββββββββββββ¬βββββββββββββββββββββββββ
β pgx (private network)
βββββββββββΌββββββββββ
β PostgreSQL 14+ β
β (bundled or host)β
βββββββββββββββββββββ
Optional companion process: a one-shot dedup CLI (same image, different entrypoint) that backfills perceptual hashes and rebuilds the duplicate-pairs table. It is not a daemon β it is run on demand.
| Component | Tech | Responsibility |
|---|---|---|
| Frontend (SPA) | SvelteKit (adapter-static, ssr=false), Svelte 5, Tailwind v4, TypeScript |
UI, client routing, PWA/offline, calls the REST API |
| API server | Go + Gin, Clean Architecture | REST API, auth, ACL, business logic, thumbnailing, audit |
| Database | PostgreSQL 14+ (pgx v5, goose) | All structured data across 4 schemas / 19 tables |
| File storage | Local disk, flat, keyed by UUID | Originals + a regenerable thumbnail/preview cache |
| dedup CLI | Go (same image) | Offline perceptual-hash backfill + pairs rescan |
| Reverse proxy | nginx (host, not shipped) | TLS termination, large-body/streaming config |
Dependencies point inward; no layer imports a layer above it.
handler β service β port (interfaces) β db/postgres, storage, imagehash
β
domain (entities, value objects, errors) β stdlib only
FileStorage, Transactor).Wiring is manual in cmd/server/main.go (no DI framework). See
GO_PROJECT_STRUCTURE.md for the file-by-file layout,
the transaction/context patterns, and the DI sketch.
Authorization: Bearer <access token> to /api/v1/..../auth) per-IP rate limiter β
auth middleware validates the JWT and puts (userID, isAdmin, sessionID)
into the request context.ctx first arg).ACLService, performs the use case β composing
repository calls inside a Transactor.WithTx when several writes must be
atomic β and writes an audit entry.ctx).{ code, message, details? } with the right HTTP status.JWT bearer auth. A short-lived access token (15 min default) authorizes API
calls; a long-lived refresh token (30 days default) rotates on use and is
stored as a hash in activity.sessions. A separate content token (6 h
default) is a single-file capability embedded in media URLs so long video keeps
streaming past access-token expiry. The /auth endpoints are rate-limited per
client IP.
Private-by-default. Admins see everything; otherwise access requires a public
flag, creator ownership, or an explicit grant in acl.permissions (read / edit).
All checks are centralized in ACLService and applied before reads and writes.
Originals are stored flat under FILES_PATH, each named by its file UUID (no
directory tree, no original-name collisions). Thumbnails and previews are a
regenerable cache under THUMBS_CACHE_PATH: still images via vipsthumbnail
(shrink-on-load) with a pure-Go imaging fallback, video frames via ffmpeg;
metadata/EXIF via exiftool with a pure-Go fallback. Uploads are rejected unless
their sniffed MIME type is whitelisted in core.mime_types.
Images are dHash-ed (64-bit perceptual hash) inline on upload; video hashes are
backfilled by the dedup CLI. A rescan rebuilds data.duplicate_pairs using a
BK-tree over Hamming distance (within DUPLICATE_HASH_THRESHOLD), and the API
groups pairs into connected-component clusters. Dismissed pairs are remembered so
they stop resurfacing. See the duplicate sections in
GO_PROJECT_STRUCTURE.md.
User-visible actions (file/tag/category/pool CRUD, relations, ACL changes,
auth, session termination, admin user actions) are recorded in
activity.audit_log against a seeded set of action types.
Pure client-side SPA: static assets served by the Go binary, with index.html
as the fallback for client routes. Installable PWA with a service worker for
app-shell caching and optional offline viewing of pinned files. See
FRONTEND_STRUCTURE.md.
PostgreSQL, four schemas (see backend/migrations/):
Migrations are goose files embedded via go:embed and applied automatically on
server startup, so a fresh database bootstraps itself.
Dockerfile builds the SPA (Node
stage) and the static Go binary (Go stage), then ships an Alpine runtime with
vips-tools / ffmpeg / exiftool and a non-root user. The server serves both the
API and the SPA on port 42776 (the sum of the code points of δΈε€).docker-compose.yml runs the app plus, optionally, a bundled
PostgreSQL (with-db profile); a host Postgres is supported by leaving the
profile empty. The app is published on loopback only and expects a host
reverse proxy; the DB sits on a private internal network with no route
off-host. The dedup CLI is a tools-profile, run-on-demand service..env.example (1:1 with config.Config). Secrets
(JWT_SECRET, ADMIN_PASSWORD, DATABASE_URL) are never baked into the image.ADMIN_USERNAME / ADMIN_PASSWORD, so setup is: fill .env,
docker compose up.See DEPLOY.md for the production deploy (Gitea Actions β host) and the reverse-proxy notes in README.md.
port interfaces (the
filter DSL β SQL translation lives in db/postgres), leaving room for other
database engines in a future version without touching the service layer.