Skip to content

Feat/production hardening - #20

Merged
codetheuri merged 4 commits into
mainfrom
feat/production-hardening
Sep 9, 2026
Merged

codetheuri merged 4 commits into
mainfrom
feat/production-hardening

Conversation

@codetheuri

Copy link
Copy Markdown
Owner

No description provided.

codetheuri and others added 4 commits September 9, 2026 16:15
…nternal/

Hardens Tusk to production grade so a downstream service can be built on it.
Developed against Salio, whose backend rewrite is the first consumer.

BREAKING CHANGES

* Primary keys are UUIDv7, generated in Go by the new pkg/id, never by the
  database. An offline-first client has to create a row and know its ID before
  the server has seen it, which no server-side default can serve. Migration
  00001 was rewritten rather than layered with a conversion; an existing
  database must be recreated. Tokens issued before this carry a numeric user_id
  and will fail to parse, so all sessions are invalidated.

* PostgreSQL is the only supported driver. MySQL and SQLite are dropped and
  LoadConfig refuses them at startup rather than failing at the first query:
  neither has a native UUID type, so their schema could no longer describe the
  same rows as the models. Row-level security is Postgres-only regardless.

* internal/middleware -> pkg/middleware, internal/app -> pkg/app, and
  internal/platform/database/gorm.go -> database/connect.go (NewGoRMDB ->
  Connect). Go's internal/ rule made these unreachable from a separate module,
  so a downstream service got pkg/* and config only. internal/auth stays
  internal deliberately: an application with different authentication should
  write its own against pkg/authz rather than fight the framework's.

* pkg/app.New takes an Options struct and registers no routes of its own.
  Callers register their modules against App.API().

Also in this change

* pkg/tenant: opt-in per-model multi-tenancy in three independent layers —
  context propagation, GORM callbacks that inject the predicate so a developer
  cannot forget it, and Postgres RLS so the database refuses cross-tenant rows
  regardless of application bugs. Opt-in because most applications are
  single-tenant and tenancy that cannot be switched off would make Tusk useless
  for them. tenant.VerifyEnforcement reports the trap that superusers and
  BYPASSRLS roles are exempt from every policy silently.

* pkg/testdb: a shared PostgreSQL harness. Database tests across packages
  serialise on an advisory lock, because go test runs packages concurrently and
  each truncates the schema — which produced failures that looked like
  application bugs.

* database.Migrator: a reusable migration runner, so a downstream service runs
  its own migrations through the same code path as the CLI. Adds Reset, which
  the round-trip migration test needed: it had been reverting a single step and,
  with only one migration in the tree, appeared to verify the whole rollback path.

* lib/pq removed. Config, the migrate CLI and the tests used it while GORM used
  pgx, so the schema was built by one driver and used by another.

* pkg/query: LIKE -> ILIKE. Searching for "wanj" did not match "Wanjiku" on
  PostgreSQL. Fixes the same latent bug in internal/auth's user search, and adds
  the package's first tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tusk is API-first, but admin panels are a recurring need and the usual answer —
ad-hoc html/template calls scattered through handlers — repeats the same
mistakes in every project. Both packages are additive; nothing else imports them.

pkg/view renders into a buffer and writes nothing until the page succeeds, so a
template error halfway through can still become a 500 instead of a truncated 200
the client believes is complete. Every page is parsed at construction, so a
malformed template stops the process starting rather than surfacing when a user
opens that page. Reload makes per-request parsing a configuration choice with a
correct default instead of a "cache this later" comment nobody revisits.

pkg/session stores only the SHA-256 digest of the token the browser holds, so a
leaked backup does not hand over live sessions. SHA-256 rather than bcrypt, for
the opposite reason to passwords: the token is 256 bits of randomness, so it is
unguessable regardless of hash speed, and it is verified on every request, where
a deliberately slow hash would be a denial-of-service surface.

Middleware loads a session without requiring one; Require refuses. Keeping them
separate is what stops the classic redirect loop, where a login page that
redirects on the mere presence of a cookie bounces a visitor holding a stale one
between /login and /dashboard forever.

Options.Kind separates audiences sharing one database, and Session.SubjectID
carries no foreign key on purpose — the subject may live in Tusk's own users
table or in an application's separate operators table, and this package has no
business deciding which.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
roadmap.md records what changed in each hardening pass and why, including the
behaviour breaks and the two findings worth carrying forward: that a superuser
is exempt from every RLS policy silently, and that the test harness had been
exercising a different driver than production.

CLAUDE.md gives an agent working in this repo the conventions and the layering
rules up front, rather than having them inferred from whichever file is open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
v1.0.0 is published, and the preceding commits are breaking: UUID primary keys,
internal/ packages promoted to pkg/, NewGoRMDB renamed to database.Connect, and
MySQL and SQLite dropped. Go resolves a module's major version from its path, so
github.com/codetheuri/tusk can only ever serve v1.x — tagging a breaking release
there would hand a broken build to anyone on v1.0.0 who ran `go get -u`, with no
warning and no way to pin around it.

The suffix is what makes the two versions independent: v1.0.0 keeps resolving for
anyone still on it, and v2 is opted into by changing an import path.

Import paths only; no behaviour changes in this commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@codetheuri
codetheuri force-pushed the feat/production-hardening branch from aaa31a9 to d570ffc Compare September 9, 2026 13:18
@codetheuri
codetheuri merged commit cc52a5f into main Sep 9, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant