Files
RenewCMS/docs/architecture.md
T

3.3 KiB

Architecture

RenewCMS is a Go HTTP API and a Vue 3 SPA, shipped as a single binary.

Backend layering

Dependencies point inward:

api/  ──▶  internal/infrastructure/useCases  ──▶  internal/domain  ◀──  internal/infrastructure/{persistence,mailer}
Layer Location Rules
Domain internal/domain/<name>/ Entities, Err… sentinel errors, Repository interfaces. Imports only the stdlib and other domain packages, with no GORM tags.
Use cases internal/infrastructure/useCases/ Application logic. Depends only on domain interfaces, never on persistence, mailer, GORM or net/http.
Adapters internal/infrastructure/persistence/{models,mappers,repositories}, internal/infrastructure/mailer Implement a domain Repository (assert with var _ domain.Repository = &X{}). GORM structs stay in models and are converted by mappers.
Delivery api/handlers/<name>/, api/router.go HTTP handlers receive use cases through NewHandler(...). They never import persistence, mailer or GORM.
Composition root api/dependecyInjection.go The only place that opens the DB, builds repositories and use cases, and lists models for AutoMigrate.
Entry point cmd/server/main.go Env/config, HTTP server, graceful shutdown.

Nothing under internal/ imports api/.

Delivery packages

Package Role
api/handlers/user Authenticated user endpoints: profile update (UpdateMe) and the users list with invitations (CreateInvite, ListUsers).
api/handlers/media The media library under /v1/media (list, upload, rename, delete, merge, usage). Images belong to the library, not to an article: deleting an article never deletes an image, and an image's "used in" is read from article bodies each time.
api/pkg/upload Multipart file reading and the mapping of upload errors (415, 413, 400) shared by both upload routes.
api/pkg/publicuser PublicUser, the JSON shape of a user with no password, codes or token version. publicuser.New converts a domain user.User, so handlers never serialise the domain struct directly.

Image metadata backfill

At boot, BackfillImageMetadata fills filename, size, content_type, width, height and sha256 for image rows that lack them, reading the files in UPLOAD_DIR. It is idempotent, and a row whose file is missing or unreadable is left as it is without stopping the boot. The old article_id column and the article_images table are kept and never written.

Frontend ↔ backend

web/ (Vue, Vite)
  │  task build:web  (vue-tsc + vite build)
  ▼
internal/infrastructure/assets/dist/        gitignored build output
  │  //go:embed all:dist   (assets_embed.go, default build)
  ▼
bin/renewcms  ──  /v1/*  API
               ──  /healthz
               ──  /*    SPA (api/handlers/frontend), index.html fallback
  • Production: a single origin. The SPA calls the API with relative URLs, so the JWT cookie works without CORS. Files under assets/ are content-hashed by Vite and cached for a year. index.html is never cached.
  • Development: built with -tags nofrontend (assets_stub.go, assets.Dist == nil). Vite serves the SPA on :5173 and proxies /v1 to the API on :8080. See development.md.