Digital Document Wallet with Integrity Verification. A personal/institutional repository for digital documents with cryptographic integrity verification via SHA-512. Users prove that a file has not been modified since its registration point by comparing a stored hash against a recomputed one.
- Document registration with automatic SHA-512 hash computation (streaming, no full-file buffering)
- Integrity verification: Intact / Divergent / Unverifiable / AlgorithmUnsupported / RecordNotFound
- Document versioning — new version linked to predecessor, prior content immutable
- Controlled sharing via expiring, revocable tokens scoped to a specific document version
- Revocation with justification and full audit trail
- Append-only audit log (INSERT-only at the PostgreSQL permission level)
- RBAC — Document Owner, Institutional Issuer, Third Party (Validator), System Administrator
| Layer | Technology |
|---|---|
| Language | Go 1.26 |
| HTML templating | templ |
| Frontend reactivity | AlpineJS |
| Styling | TailwindCSS 4 |
| Partial updates | HTMX |
| JS/CSS bundler | Bun |
| Database | PostgreSQL |
| DB access | sqlc + pgx/v5 |
| Migrations | goose |
| Router | chi |
| Hot reload | air |
| Task runner | just |
- mise (manages Go, Bun, just)
- Docker (for local PostgreSQL via docker compose)
Install tool versions:
mise install# Install JS dependencies and Go tool dependencies
just deps
# Start local PostgreSQL
just db-up
# Run migrations
just migrateCopy .env.example to .env (once it exists) and set:
DSN=postgres://user:password@localhost:5432/docstore?sslmode=disable
just devThis starts five parallel watchers:
| Watcher | What it does |
|---|---|
live/templ |
Regenerates templ components, proxies :8080, hot-reloads browser |
live/server |
air — rebuilds and restarts Go binary on .go changes |
live/tailwind |
Rebuilds out.css on CSS changes |
live/esbuild |
Bundles index.js → web/static/js/ on JS changes |
live/sync-assets |
Notifies templ proxy on JS/CSS asset changes |
The app listens on :8080; the browser dev proxy is on :7331.
just build # production binary (./cmd/server)
just generate # templ + sqlc codegen
just test # full test suite with race detector
just test-one NAME # single test: just test-one TestFooBar
just migrate # apply pending migrations (goose up)
just migrate-status # show migration status
just migrate-down # roll back one migration
just css # one-shot Tailwind build
just deps # install all dependenciesCode is organized by feature (vertical slice), not by technical layer. Each slice owns its handler, service logic, and types end-to-end.
cmd/server/ # main entrypoint — registers routes, wires slices
internal/
upload/ # document registration + hash (RF01–RF03)
verify/ # integrity check (RF05)
versioning/ # new version linked to predecessor (RF07)
revocation/ # revoke with justification + audit (RF08)
sharing/ # token generation, expiry, public verify (RF06)
collection/ # type/tag/collection organization (RF04)
receipt/ # integrity receipt issuance (RF09)
export/ # export document + integrity records (RF12)
importdoc/ # import with external hash (RF13)
expiration/ # background worker — no handler (RF11)
platform/
auth/ # User, Role, password hashing
session/ # HMAC-signed PostgreSQL session store
audit/ # append-only audit writer
store/
db/ # sqlc-generated code (do not edit by hand)
migrations/ # goose files (sequential: 00001_*.sql)
queries/ # sqlc SQL source (one file per slice)
testhelper/ # shared testcontainers-go + goose runner
web/
templates/ # .templ files mirroring slice names
static/ # Bun build output (CSS, JS)
Slices do not import each other. Shared types come from store/db models or internal/types.go.
upload/
handler.go # chi handler — reads request, calls service, renders templ
service.go # business logic — depends on store/db.Querier + platform/audit
types.go # request/response types, view models, domain errors
Dev tools live in tools/ with their own go.mod to keep the main module clean:
# Run a tool directly
go tool -modfile=tools/go.mod templ generate
go tool -modfile=tools/go.mod sqlc generate
# Add a new tool
cd tools && go get -tool github.com/some/tool@latestgo generate ./... uses //go:generate directives that invoke tools via -modfile=tools/go.mod.
Local PostgreSQL is managed via docker compose:
just db-up # start
just db-down # stopMigrations use goose with sequential numeric naming (00001_create_users.sql). Never edit an applied migration — always add a new file.
All DB access goes through sqlc-generated code in store/db/. No raw query strings in .go files.
Integration tests via testcontainers-go are the primary testing strategy. Each test spins up a real PostgreSQL container, runs all migrations, and runs inside a pgx.Tx that is rolled back in t.Cleanup.
Unit tests are reserved for pure logic with zero I/O: hash computation, HMAC verification, TTL math.
just test # all tests, race detector, no cache
just test-one TestVerifyHash # single testSessions use opaque random tokens — a DB breach yields hashes that cannot be reversed to valid cookie values:
- Cookie value:
hex(crypto/rand 32 bytes)— 256 bits of entropy - DB stores
sha256(token)— the raw token never persists server-side - Invalid or tampered tokens simply miss in the DB
Upload → io.TeeReader(file, sha512.New()) → write bytes to storage
→ single pgx.Tx:
INSERT documents
INSERT integrity_records (version_id, algorithm, digest, computed_at)
INSERT audit_log (actor_id, action, ref_id, ts)
Verify → fetch stored (digest, algorithm) for version_id
→ recompute digest (streaming)
→ subtle.ConstantTimeCompare
→ Result: Intact | Divergent | Unverifiable | AlgorithmUnsupported | RecordNotFound