Files
mygo/docs/decisions.md
T
ld 63ede5c237 refactor(api): enforce protocol-neutral service boundaries
- refactor: move HTTP status and DTO concerns out of model and service
  layers into API and handler code.
- feat: add admin service boundary and route auth through services
  instead of direct repository access.
- test: add architecture and error tests covering package boundaries,
  redaction, and service behavior.
- docs: record the protocol-neutral boundary decision and update
  architecture and roadmap notes.
2026-07-05 23:30:17 +08:00

8.6 KiB

Technical Decisions

2026-04-25: v0 Tech Stack & Architecture

Context: Project skeleton was created with only cobra CLI. We needed a concrete tech stack and package layout to begin implementation.

Decisions:

Area Choice Rationale
HTTP framework Gin Most widely adopted Go web framework, mature middleware ecosystem
ORM GORM SQLite-first dev, PostgreSQL option later; GORM abstracts dialect differences
Config management Viper YAML + env vars + CLI flags three-way merge, built for cobra integration
Database SQLite (v0) → PostgreSQL (future) SQLite zero setup for dev; repo interface isolates the switch
File storage Local disk (v0) → S3 (future) Backend interface (internal/storage) hides implementation
File identity UUID Distributed-friendly, no coordination needed; cost is negligible for file metadata
Token strategy JWT, refresh token stored in DB Enables server-side revocation (admin kick, logout-all-devices)
Pagination OFFSET/LIMIT Simple, sufficient for v0; migrate to cursor-based if needed
API response format Direct JSON success bodies + unified error body HTTP status codes carry request outcome; error body carries human-readable details

Architecture: Four-layer model — Handler (Gin) → Service (business logic) → Repository (GORM data access) + Storage (file I/O). Each layer depends only on interfaces of the layer below.

Consequences:

  • Handler layer has no business logic; Service layer is reusable across REST API, WebDAV, and future Nextcloud API.
  • Repository interfaces keep DB swappable; future PostgreSQL implementation only needs a new package.
  • Refresh token in DB adds a sessions table and a repository.SessionRepository interface.
  • UUID dependency: github.com/google/uuid to be added.
  • Gin middleware chain: default logger/recovery → cors → auth (route-group-scoped).

2026-04-27: Web API Foundation

Context: The project needed the first HTTP slice that can validate Gin wiring and provide a stable shape for future auth, file, and admin APIs.

Decisions:

Area Choice Guidance
API versioning All REST routes under /api/v1 Keep future REST handlers under the versioned group.
Initial public endpoint GET /api/v1/version Returns build metadata only; health/readiness endpoints need a separate security review.
Success responses Direct JSON resource bodies Use HTTP status codes as the request outcome signal.
Error responses {"error":{"message":"..."}} Add machine-readable error codes only when clients need stable branching behavior.
App composition internal/app.WebApp cmd/serve.go creates the app from config and build metadata, then passes it to router setup.
Router setup internal/server.NewRouter(*app.WebApp) Public routes (routes_public.go) and protected routes (routes_protected.go) split by auth boundary; WebApp serves as the unified dependency container.
Server lifecycle RunWithGracefulShutdown Preserve graceful shutdown while keeping command startup linear.
Default middleware gin.Default() Use default logger/recovery for the skeleton; add CORS/auth explicitly when their policies exist.

Consequences:

  • Version is build metadata from internal/app/version.go, not a config-file field.
  • app.WebApp is the place to add future services, repositories, storage, and app metadata incrementally.
  • Request ID middleware is not part of the current foundation; add it only with a logging/tracing/error-correlation design.

2026-04-29: Auth Refinements

Context: Auth layer had three structural weaknesses — handler duplication, indistinguishable token types, and fragile config duration parsing.

Decisions:

Decision Guidance
One handler per route group AuthHandler owns /auth/* (public); AccountHandler owns /account/* (protected). A route group maps 1:1 to a handler type.
JWT type claim Claims.Type distinguishes access from refresh tokens. Middleware and service enforce the correct type at their respective boundaries. ParseToken does no type check — it verifies cryptographic validity only.
Default JWT secret hardening The development placeholder jwt.secret is replaced with an ephemeral runtime secret during config loading. Production and multi-instance deployments must set a stable secret.
time.Duration in config structs Config fields representing durations use time.Duration directly. Viper's built-in StringToTimeDurationHookFunc handles string→Duration conversion at unmarshal time. No accessor methods, no runtime parsing. Invalid values fail at startup via Load().

Consequences:

  • Handlers are independently extensible (caching, rate limiting scoped per handler).
  • Refresh tokens cannot authenticate API requests; access tokens cannot be used to issue new token pairs.
  • The placeholder JWT secret is safe for local startup, but tokens signed with it are invalidated on restart.
  • New duration config fields require zero boilerplate — declare as time.Duration in the struct.

2026-07-05: Staged File Uploads

Context: Multipart uploads previously used ParseMultipartForm, which parses the request before service-level size checks and may spill oversized requests to temporary disk. The file service also wrote directly to the long-term storage path and then attempted compensating deletes on upload failure.

Decisions:

Decision Guidance
Stream multipart requests Handlers use Request.MultipartReader() instead of ParseMultipartForm/FormFile, so uploads are streamed into the service.
Optional upload limits storage.max_upload_size = 0 means unlimited. Positive values enable both HTTP body limiting and service-level file content limiting.
Staging before promotion Storage backends write upload bytes to a staging path first, then promote the object to the long-term data path only after validation succeeds.
Promote before DB create The service promotes the object before creating the active file record, preventing visible DB rows from pointing at missing objects. If DB creation fails after promotion, the service best-effort deletes the promoted object.
Upload parent location Multipart upload parent_id is passed as a query parameter, keeping the multipart body focused on the file stream.
Service-layer upload errors HTTP transport errors are converted at the handler boundary; the file service only handles domain errors such as oversized uploads.

Consequences:

  • Interrupted, malformed, and oversized uploads leave only staging objects, which are safe to clean by path prefix and age.
  • Local storage promotes with os.Rename and falls back to copy/delete on cross-device EXDEV; future S3 storage can implement promotion with copy/delete while keeping business visibility controlled by the DB row.
  • A DB failure after promotion can still leave a long-term orphan object, but it is not visible through the file API and can be cleaned independently.

2026-07-05: Protocol-Neutral Service Boundary

Context: The service and model layers carried HTTP status codes and JSON response tags, and some middleware and handlers accessed repositories directly. That made the business layer harder to reuse for future WebDAV and Nextcloud-compatible APIs.

Decisions:

Decision Guidance
Protocol-neutral errors model.AppError uses an error Kind, safe message, and optional cause. REST status codes are mapped in internal/api only.
API log references REST error responses may include error.log_id for server-recorded errors: all 5xx responses and 4xx responses with internal causes.
Service boundaries Handlers and middleware depend on services, not repositories. AuthService authenticates access tokens into a principal; AdminService owns admin user operations.
DTO ownership Service and model structs are internal/domain data. HTTP handlers assemble response DTOs with JSON tags.
Architecture enforcement Package-level tests reject HTTP imports in model/service and repository imports in handlers/middleware. Targeted model tests verify defensive JSON redaction for sensitive fields.

Consequences:

  • REST remains a handler/API concern, and future protocols can reuse services without HTTP leakage.
  • Error responses keep the same top-level shape, with optional log_id instead of embedding log references in the message.
  • Admin and auth middleware behavior is testable through service contracts rather than database access.