Feat/production hardening - #20
Merged
Merged
Conversation
…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
force-pushed
the
feat/production-hardening
branch
from
September 9, 2026 13:18
aaa31a9 to
d570ffc
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.