- Go 82.5%
- Svelte 8.7%
- TypeScript 6.3%
- JavaScript 1.2%
- Shell 0.4%
- Other 0.9%
terraform apply failed with 'server type cx22 not found': the current shared x86 line is cx23/cx33/cx43/cx53 (verified against the live /v1/server_types catalog), not the cx22/cx32 naming used in the SCM and monitoring box defaults. Docs and examples follow. |
||
|---|---|---|
| .github | ||
| .semgrep/rules | ||
| .woodpecker | ||
| api | ||
| branding | ||
| cmd | ||
| deploy | ||
| docs | ||
| internal | ||
| migrations | ||
| scripts | ||
| web | ||
| .air.toml | ||
| .air.worker.toml | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .golangci.yml | ||
| .lychee.toml | ||
| .markdownlint-cli2.jsonc | ||
| docker-compose.yml | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| lefthook.yml | ||
| Makefile | ||
| mise.toml | ||
| README.md | ||
| SECURITY.md | ||
| sqlc.yaml | ||
Scut
A risk-first security posture platform. Where Vanta and Drata are certification-first, Scut centers on risk: deep onboarding to understand a business, its industry, and its tech stack, then a continuously maintained risk register, threat-intel correlation, and a pragmatic threat-exposure map. Compliance scoring and automated tests are table stakes we also ship. The differentiator is real-time risk grasp.
Status: the platform is feature-complete and stable. In place: onboarding & risk assessment, the risk register (mitigations, controls, residual scoring), threat-intel correlation, governed multi-provider AI, the passive scan engine (ownership-verified), third-party/vendor risk (TPRM), executive reporting & posture trends, the Ask Scutty AI risk analyst, a hardened multi-tenant API, OpenTelemetry observability, integrations (GitHub SCM + SAML SSO), notifications & alerting, NIST CSF compliance, a public API with webhooks, a two-sided security-services marketplace, threat modeling, architecture-diagram ingestion, a consolidated design system, and an in-house penetration test with remediation. See the documentation index and the architecture overview for the full picture.
Architecture overview
Scut is a modular monolith: a single Go binary (cmd/server) plus a worker
entrypoint (cmd/worker), organized by domain module. The frontend and backend
are separate deployables that communicate only over a versioned HTTP/JSON API described by an
OpenAPI contract. That contract is the decoupling boundary. Any module could later be
extracted into its own service without a rewrite.
Stack
| Layer | Choice | Notes |
|---|---|---|
| Backend | Go + chi |
small, idiomatic net/http router |
| Database | PostgreSQL + Row-Level Security | the only infra dependency for the MVP |
| DB access | pgx + sqlc |
type-safe Go generated from SQL |
| Migrations | goose |
plain SQL up/down |
| Jobs | river |
Postgres-backed background workers |
| API contract | OpenAPI 3.0.3 | source of truth for FE client + BE types |
| Frontend | SvelteKit 2 + Tailwind 4 | bits-ui components, layerchart charts, @auth/sveltekit sessions |
| LLM | Pluggable providers (Anthropic, OpenAI) | server-side only; BYO keys, per-org model governance |
| Identity | Zitadel (self-hosted, OIDC) | org ID maps to app org_id |
Tech stack, explained
- Go modular monolith. One codebase, one deploy, domain modules with hard interface
boundaries: the operational simplicity of a monolith now, with clean seams to extract a
service later.
chikeeps routing to stdlibnet/httpidioms with no framework lock-in. - PostgreSQL is the whole backend infra. Tenancy (Row-Level Security), background jobs
(
river), and the LLM usage ledger all live in Postgres: no Redis, no separate queue, no message bus to operate for the MVP. Tenant isolation is enforced in the database by RLS policies, not just in application code. pgx+sqlc. Queries are written as plain SQL and compiled to type-safe Go, so the compiler catches schema drift.goosemigrations are plain reversible SQL.riverjobs. Durable, Postgres-backed background work (extraction, scanning, threat-intel correlation, batched LLM calls) runs in a separate worker entrypoint that shares the same modules as the API.- OpenAPI as the contract.
api/openapi.yamlis the single source of truth.make genderives the Go server types and the SvelteKit client from it, so the frontend and backend can never silently diverge. - SvelteKit frontend. A separate deployable that holds no secrets and talks to the
backend only through the generated, fully-typed OpenAPI client, never directly to the LLM
or the database. Styled with Tailwind 4,
bits-uiprimitives, andlayerchartfor posture trend charts. - Pluggable LLM layer. A provider-agnostic seam (
internal/platform/llm) supports Anthropic and OpenAI, with bring-your-own keys, a per-org model-governance policy, a usage ledger, prompt caching, and right-sizing. Extraction proposes; humans dispose (a review queue, never auto-commit). All LLM calls are server-side only. - Zitadel for identity. Self-hosted OIDC owns login, Google sign-in, MFA, and tokens; the app owns domain roles and all tenant data.
Non-negotiable rules
These are enforced in code review and CI:
- Tenant isolation. Every tenant-owned table has
org_idand an RLS policy; every tenant-scoped query runs under the tenancy context. New tenant-scoped resources ship with a test proving cross-tenant access fails. - Module boundaries. Modules depend on each other's interfaces, never on another
module's repository or tables. Only
internal/platformtouches infra directly. - Secrets live in env/secret-manager: never in code, never sent to the frontend.
- LLM extraction proposes; humans dispose. Extracted assets/risks land in a review queue, never auto-committed to the register.
- Active scanning only runs against ownership-verified assets.
Identity & tenancy
Zitadel is the authority for identity (users, login, OIDC, MFA, tokens). A Zitadel
organization ID maps 1:1 to the app's org_id used by Postgres RLS. The app database owns
all domain data and the app roles (owner / admin / analyst / viewer); it trusts
Zitadel only for "who are you and which org."
Every tenant-scoped request flows through one chain:
auth (validate token, extract org + subject)
→ tenancy tx (sets app.current_org so RLS applies)
→ RequireRole (app-owned RBAC)
→ handler
→ store + append-only audit
Repository layout
scut/
cmd/
server/ # API binary
worker/ # background worker (same modules, different entrypoint)
migrate/ # applies the river job schema (after goose)
seed/ # dev-only mock data
internal/
platform/ # cross-cutting infra: config, logging, db, auth, tenancy ctx, jobs, secrets, llm, otel
<domain modules> # tenancy, onboarding, assets, risk, scan, threatintel, threatmodel,
# vendors, compliance, reporting, posture, exercises, scutty, marketplace,
# providers, integrations, notify/notifications, uploads, aikeys, apikeys, ...
api/ # openapi.yaml (the FE/BE contract, source of truth)
migrations/ # goose SQL migrations (tables + RLS policies)
web/ # SvelteKit frontend (separate build)
deploy/postgres/ # initdb bootstrap (scut DB + least-privilege scut_app role)
docs/ # developer + operator documentation (see docs/README.md)
The full module map (all 27 internal/ packages, one line each, with boundaries) is in
docs/architecture.md.
Local development
Prerequisites
- mise: provisions the pinned Go + Bun toolchain (
mise.toml) - Docker + Docker Compose
make
mise trust && mise installinstalls Go 1.26 and Bun 1.3.14 at the versionsmise.tomlpins, so your local toolchain matches CI. (If you'd rather manage them yourself: Go 1.26+ and Bun 1.3+.)
Quickstart
# 1. Clone
git clone https://github.com/frameward/scut.git && cd scut
# 2. Install the pinned toolchain (Go + Bun)
mise trust && mise install
# 3. Configure environment (insecure local defaults; adjust if you like)
cp .env.example .env
cp web/.env.example web/.env
# 4. Bring up Postgres + Zitadel (behind a Traefik proxy)
docker compose up -d --wait
# 5. Configure Zitadel (ONE-TIME, see docs/zitadel-setup.md).
# Create the scut-web app and a test user, then set the client ID in BOTH:
# .env → SCUT_ZITADEL_CLIENT_ID
# web/.env → AUTH_ZITADEL_ID (+ generate AUTH_SECRET)
# 6. Apply database migrations
make migrate
# 7. Install frontend deps and git hooks (hooks: contributors)
cd web && bun install && cd ..
make hooks
# 8. Run the backend and frontend (separate terminals)
make run # Go API → http://localhost:8081
make web-dev # SvelteKit → http://localhost:5173
# 9. Open the app
open http://localhost:5173
Ports
| Service | URL | Port |
|---|---|---|
| Frontend (SvelteKit) | http://localhost:5173 | 5173 |
| API (Go) | http://localhost:8081 | 8081 |
| Zitadel console (via Traefik) | http://localhost:8080/ui/console | 8080 |
| Postgres | localhost | 5432 |
See docs/local-env.md for the full stack (the least-privilege scut_app
role, the app database, compose lifecycle) and docs/zitadel-setup.md
for the one-time identity setup.
Common commands
make help lists every target. The essentials:
| Command | What it does |
|---|---|
make run |
Run the API server once (loads .env) |
make dev |
Run the API server with live reload on Go changes (air) |
make worker |
Run the background worker once |
make dev-worker |
Run the worker with live reload (air) |
make migrate |
Apply database migrations (goose up) |
make migrate-status |
Show migration status |
make gen |
Regenerate sqlc, Go API types, and the FE client |
make test |
Run the whole Go suite against an isolated scut_test DB (safe alongside the dev stack) |
make test-db |
Create + migrate the isolated scut_test database (idempotent) |
make check |
go vet + golangci-lint (the same lint CI runs) |
make hooks |
Install git hooks (lefthook); run once after cloning |
make web-dev |
Start the SvelteKit dev server |
make web-check |
Type-check the SvelteKit app |
Code generation
api/openapi.yaml is the FE/BE contract. make gen regenerates everything
derived from a source of truth:
- sqlc: type-safe Go from SQL (
internal/**/store/) - oapi-codegen: Go request/response types (
internal/api/types.gen.go) - openapi-typescript: the SvelteKit client types (
web/src/lib/api/schema.d.ts)
Generated files are committed and must not be hand-edited: change the source and rerun
make gen.
Testing
make test # the whole suite, against an isolated scut_test database
make test runs everything (unit tests plus the DB-backed integration and acceptance
tests) and it does so against a dedicated scut_test database that make test-db
auto-creates and migrates (idempotently). Two properties matter:
- It is isolated from the dev stack. The background worker (
make worker/make dev-worker) polls thescutdatabase; the suite runs onscut_test. Their River job queues never overlap, so a running worker can't grab and reprocess the jobs the tests enqueue.make testis safe to run alongside a livemake dev/make dev-worker: there is nothing to stop first. (Running the suite against the same database a worker polls is the classic source of "flaky" enqueue/lifecycle failures; this design removes that class entirely.) - It connects as the non-superuser
scut_approle. Superusers bypass Row-Level Security, which would turn tenant-isolation assertions into false passes.scut_testis owned byscut_appand every tenant table isFORCE ROW LEVEL SECURITY, so RLS enforces even for the owner, exactly as in production.
CI (or any custom setup) can point the suite at a different database by exporting
SCUT_TEST_DATABASE_URL before go test ./...; make test only supplies the local default. The
DB-backed tests skip cleanly when no test database URL is available.
We aim for 100% relevant coverage, not 100% coverage. Always tested: tenant isolation, RBAC, risk scoring, extraction→register mapping, scanner normalization, integration adapters. Generated code and trivial DTOs are not tested. There is no coverage-percentage gate; see the contributing guide.
Contributing
Workflow. Spec first, then an ordered plan, then code. Keep commits small and reviewable; one vertical slice at a time. If execution diverges from the plan, surface it rather than improvising silently.
Before you push. Run make hooks once after cloning. lefthook
then runs, on every commit, the same checks CI runs (no version drift; golangci-lint is
pinned in go.mod):
- pre-commit:
gofmt+golangci-linton changed Go;svelte-checkwhenweb/changes. - pre-push:
go test ./...(the unit suite).
CI. Self-hosted Woodpecker runs on every push to main and
every PR. The pipeline lints (gofmt + golangci-lint, go mod tidy clean), builds, applies
migrations against an ephemeral Postgres, and runs the full -race test suite, including the
integration tests that exercise RLS as the non-superuser scut_app role. It also type-checks
the SvelteKit app and runs a security sweep: govulncheck, gosec, semgrep, a frontend
dependency audit (gated on high/critical), and a TruffleHog secret scan. The Woodpecker pipeline
status gates branch protection. See the workflows under .woodpecker/ and the
Hetzner + Woodpecker runbook.
Conventions. Commit messages use conventional prefixes (feat, fix, test, ci,
docs, ...). Open PRs against main. The module-boundary and non-negotiable rules above are
enforced in review.
Full docs live under docs/: the documentation index is the
place to start. Highlights:
- docs/architecture.md: the module map, request lifecycle, tenancy/RLS, and LLM governance
- docs/data-model.md: the core entities and an ERD
- docs/decisions/: Architecture Decision Records (the why)
- docs/contributing/: setup, the tenant-slice pattern, migrations, propose-dispose
- docs/configuration.md: every
SCUT_*environment variable - docs/deployment.md: what you deploy and how; production checklist
- docs/local-env.md · docs/zitadel-setup.md: local stack + one-time identity setup
License
No LICENSE file has been added yet; until one is, treat this repository as
internal / all rights reserved.