interfaces - refactor: split UserRepository into AuthUserRepository and AdminUserRepository capabilities - refactor: split SessionRepository into AuthSessionRepository and repository-owned CLI/admin methods - refactor: replace generic Repository Update/Delete with operation-specific params and ownership predicates - refactor: replace CredentialRepository generic CRUD with passkey-specific methods - refactor: replace FileRepository generic Create/Update/Delete with UploadedFileParams, DirectoryParams, and owned soft-delete - refactor: remove repository fields from WebApp struct; repositories are now composition-time wiring only - feat: add domain error kinds ErrParentNotFound, ErrParentNotDir, ErrDirectoryNotEmpty, ErrInvalidMove - feat: add CredentialTypeAppPasskey constant - feat: add testutil.SetUserAdmin for test fixture setup that bypasses production service ports - test: add architecture test banning GORM Save in repository package - test: add capability interface contract tests ensuring each service receives the minimal interface - test: add blockingStorage helper for concurrent promotion tests - test: add preserved DSN parameter test for sqliteImmediateDSN - docs: update architecture decisions with repository write rules and capability separation - docs: update roadmap to clarify atomic single-use refresh sessions - docs: add -race test target to development docs
6.6 KiB
Architecture
Layered Design
Handler (Gin handlers) ← translates HTTP ↔ Service calls
↓
Service (business logic) ← orchestrates, authorizes, validates
↓ ↓
Repository (GORM data access) Storage (file I/O)
↓ ↓
[SQLite / PostgreSQL] [Local FS / S3]
Rules:
- Handler has no business logic — parse request, call service, write response.
- Handler and middleware never access repositories directly; they depend on services.
- Service has no HTTP awareness — operates on domain models and interfaces.
internal/modelandinternal/servicedo not importnet/http, Gin, orinternal/api.- JSON response DTOs live in HTTP packages. Domain and service result structs are not the public REST schema.
- Domain models may use
json:"-"for defensive redaction of sensitive fields; this is not a REST response contract. - Domain errors carry a protocol-neutral kind, a safe message, and an optional cause. HTTP status mapping happens only at the API boundary.
- Repository query ports remain entity-oriented, while mutation ports expose business commands with operation-specific parameter types. Services never pass a mutable domain entity to a generic update or delete method.
- Service constructors using split repository capabilities accept the smallest capability needed by that service. Their composition-time unions do not cross into business services.
- Repository mutations use explicit columns and lifecycle/ownership predicates. Full-entity
Saveis prohibited so a write cannot accidentally change protected fields or revive a soft-deleted record. - Repository abstracts the database; Storage abstracts where bytes live, including staged upload promotion.
internal/serveris the composition root — wires all dependencies together.
Package Map
| Layer | Package | Purpose | Status |
|---|---|---|---|
| CLI | cmd |
Cobra root command | 🛠 WIP |
cmd/serve.go |
mygo serve — wire deps, start HTTP |
✅ | |
cmd/config.go |
mygo config — config subcommand |
🛠 WIP | |
cmd/status.go |
mygo status — health check |
🛠 WIP | |
| Config | internal/config |
Viper load (YAML + env + flags), typed Duration config via built-in decode hook | ✅ |
| App | internal/app |
Runtime dependency container and build metadata | 🛠 WIP |
| HTTP | internal/server |
Gin router init, route registration (public/protected split), graceful shutdown | ✅ |
internal/handler |
HTTP handlers (auth, file, admin, webdav...) | 🛠 WIP | |
internal/middleware |
Gin middleware (logger, jwt, cors, auth) | 🛠 WIP | |
| Business | internal/service |
Business logic: AuthService, FileService, AdminService; protocol-neutral results and errors |
✅ |
internal/model |
Domain types (User, File, Credential, Session), protocol-neutral error kinds | ✅ | |
| Data | internal/repository |
Repository interfaces + GORM implementations (User, Session, File, Credential) | ✅ |
internal/storage |
Storage backend interface + local disk impl, with staging and promotion | 🛠 WIP | |
| Util | internal/auth |
JWT sign/verify (HS256), token type discrimination (access/refresh), password hashing (bcrypt), app passkey tokens | ✅ |
internal/api |
Unified JSON error response helpers and REST error-kind mapping | ✅ | |
internal/testutil |
Test-only fixture helpers for privileged setup that production service ports intentionally do not expose | ✅ |
Persistence Write Boundaries
- User registration accepts
RegisteredUserParams; the repository fixes new users to non-admin, active state. Administrative deletion is a separate capability. - File creation, directory creation, metadata changes, and soft deletion use distinct commands. Metadata updates can only write
name,parent_id, and GORM-maintainedupdated_at. - Refresh sessions are created, atomically consumed, idempotently revoked, or removed by explicit maintenance operations. Refresh token rotation consumes a session at most once.
- Passkey creation fixes the credential type to the app-passkey type. Usage recording and revocation include credential-type and ownership conditions where applicable.
app.WebAppcontains runtime services, storage, database lifecycle, configuration, and metadata. Repository implementations remain local to bootstrap and are injected directly into the services that require them.
File Hierarchy Transactions
File child creation, movement, and directory deletion share one parent-locking protocol. PostgreSQL locks active owned source and parent rows in sorted ID order with SELECT ... FOR UPDATE; directory deletion checks active children while holding the directory lock. SQLite connections add _txlock=immediate to the configured DSN so the transaction acquires its write reservation before reading hierarchy state. These rules prevent a concurrent mutation from creating an active child below a deleted directory.
API Routes (v0)
GET /api/v1/version
POST /api/v1/auth/register
POST /api/v1/auth/login
POST /api/v1/auth/refresh
POST /api/v1/auth/logout
GET /api/v1/account
GET /api/v1/account/passkeys
POST /api/v1/account/passkeys
DELETE /api/v1/account/passkeys/:id
GET /api/v1/files
POST /api/v1/files # JSON creates directories; multipart uploads files. File upload parent_id is a query parameter.
GET /api/v1/files/:id
GET /api/v1/files/:id/content
PUT /api/v1/files/:id
DELETE /api/v1/files/:id
GET /api/v1/admin/users
GET /api/v1/admin/users/:id
PUT /api/v1/admin/users/:id
DELETE /api/v1/admin/users/:id
Middleware Chain
Applied globally by gin.Default(): logger → recovery
Planned globally: cors
Applied to protected groups: auth (extract bearer token, delegate access-token authentication to AuthService, inject principal into gin.Context)
Applied to admin groups: admin (check authenticated principal from context)
Server Responsibilities
cmd/serve.goloads config, callsapp.Bootstrapto initialize DB + services, builds the router, and starts the HTTP server.app.WebAppcarries runtime dependencies and build metadata needed to assemble handlers.internal/serverowns Gin router setup (router.go), route registration split intoroutes_public.go(public auth) androutes_protected.go(JWT-protected account).- Each route group creates its own handler instance:
routes_public.gocreatesAuthHandler,routes_protected.gocreatesAccountHandler— no shared handler state between public and protected routes. RunWithGracefulShutdownstops accepting new requests on termination and gives in-flight requests time to finish.