Scut, a risk-first security posture platform
  • Go 73%
  • TypeScript 12.4%
  • Svelte 10%
  • JavaScript 2.5%
  • Shell 0.7%
  • Other 1.3%
Find a file
Marcello Evangelista 40896fd431 feat(web): Melly answers as it writes, and echoes what you asked
A send was one blocking round trip. `?/send` read the entire SSE reply
with parseAs: "text" before the action returned, so for the whole of the
model's latency the reader's question sat in the composer, the transcript
did not contain it, and no part of the answer existed until all of it did.
The backend was never the problem: agent.go emits a token frame per token
and chat.go flushes each one. Only the browser could not see them.

/melly/stream and /admin/melly/stream forward that reply straight through,
the same shape as the incident and tabletop SSE proxies beside them - the
token stays server-side and the frontend never talks to the backend. The
helper they use, requireSessionToken, still carries a doc comment naming
"the Ask Melly SSE stream" as its reason for existing; the route it named
was deleted in the #999 rebuild and this is it, back.

ChatBlock.sendStream opts a surface in. The composer posts the message
itself, puts the reader's turn up immediately, and grows the reply as the
frames arrive; `?/send` is untouched and is still what runs with
JavaScript off, so persistence and the fallback are unchanged. The layer's
"a write is a form post" rule is not repealed - isChat gets the same
explicit, per-surface escape FieldsBlock has with `progressive`, for the
one payload that genuinely arrives in pieces.

One frame decoder, shared: the buffered parser and the live reader fold
the same bytes the same way, so the two paths to the same feature cannot
disagree about what arrived. The reader keeps a partial frame in a buffer,
because a network chunk cuts frames in half.

Four defects found while building it, each now with a gate:

- The composer cleared when the REPLY finished, leaving the sent question
  in the box for a minute with Enter one keystroke from re-asking it.
  `accepted()` clears it when the server takes the message instead.
- Clearing the streamed turn on any `data` change threw it away on the
  send's own reload, dropping the consulted-tool chips and proposal cards
  from every streamed send.
- Handing the turn to the page while still rendering it in the block
  showed the reply bubble twice for the length of the reload.
- A memory disposal carries no turn of its own, so falling back to the
  streamed one put the disposed proposal card back on screen, still
  offering Confirm and Dismiss.

The scroll effect's premise - "there is no later frame that could arrive"
- is exactly what this removes. It now sticks to the bottom while a reply
streams and releases the moment the reader scrolls up.
2026-08-15 21:12:31 +02:00
.dagger fix(build): bump Go to 1.26.6 across every pin 2026-08-14 01:52:52 +02:00
.semgrep/rules refactor(module): rename Go module path to github.com/frameward/viezure 2026-08-04 10:48:36 +02:00
.woodpecker chore(deploy): the prod stack takes the product's name, the identifiers under it keep theirs 2026-08-15 14:14:51 +02:00
api feat(web): every insider code says its name - the legend layer 2026-08-14 05:11:17 +02:00
branding feat(brand): adopt the delivered badger artifacts and the parchment palette 2026-08-04 10:33:41 +02:00
cmd fix(notifications): the fresh-eyes round's nine findings, each with its gate 2026-08-14 02:28:34 +02:00
demo/companies fix(demo): drop emphasis-as-heading in Polygonal profile (MD036) 2026-07-24 01:08:05 +02:00
deploy chore(deps): update deployment images 2026-08-15 14:55:55 +02:00
docs refactor(web): the assessment says where you are once, across the top 2026-08-15 21:12:14 +02:00
internal feat(web): the landing is one numbered edition, and it carries its own receipt 2026-08-15 13:39:28 +02:00
migrations feat(risk): carry a suggested mitigation's price through approval 2026-08-09 08:41:54 +02:00
scripts chore(deploy): the prod stack takes the product's name, the identifiers under it keep theirs 2026-08-15 14:14:51 +02:00
tools feat(concrete): seed the August 2026 framework catalog 2026-08-05 06:59:56 +02:00
web feat(web): Melly answers as it writes, and echoes what you asked 2026-08-15 21:12:31 +02:00
.air.toml chore(dev): live-reload the API + worker with air 2026-06-29 00:14:59 +02:00
.air.worker.toml chore(config): remove em-dashes from deploy, Docker, and tooling config 2026-07-03 19:29:03 +02:00
.dockerignore chore(config): remove em-dashes from deploy, Docker, and tooling config 2026-07-03 19:29:03 +02:00
.env.example chore(deps): update deployment images 2026-08-15 14:55:55 +02:00
.gitignore feat(blocks): say something when a purple button is clicked 2026-08-09 12:30:18 +02:00
.golangci.yml chore: enforce module boundaries and size limits with linters 2026-07-08 21:28:41 +02:00
.lychee.toml docs: repoint rule citations at tracked docs instead of gitignored files 2026-07-20 00:22:05 +02:00
.markdownlint-cli2.jsonc docs: repoint rule citations at tracked docs instead of gitignored files 2026-07-20 00:22:05 +02:00
dagger.json chore(deps): update dagger to v0.21.8 2026-08-04 10:49:25 +02:00
docker-compose.yml refactor(module): rename Go module path to github.com/frameward/viezure 2026-08-04 10:48:36 +02:00
Dockerfile fix(build): bump Go to 1.26.6 across every pin 2026-08-14 01:52:52 +02:00
go.mod chore(deps): update module golang.org/x/mod to v0.40.0 [security] 2026-08-15 13:54:32 +02:00
go.sum chore(deps): update module golang.org/x/mod to v0.40.0 [security] 2026-08-15 13:54:32 +02:00
lefthook.yml chore: add secret, dependency, drift, and convention gates to pre-push 2026-07-26 12:50:49 +02:00
Makefile feat(web): the landing is five sections, and the proof is the record itself 2026-08-14 03:45:02 +02:00
mise.toml fix(build): bump Go to 1.26.6 across every pin 2026-08-14 01:52:52 +02:00
README.md chore(brand): regenerate every brand-bearing screenshot 2026-08-04 12:08:44 +02:00
renovate.json refactor(module): rename Go module path to github.com/frameward/viezure 2026-08-04 10:48:36 +02:00
SECURITY.md docs: state the portfolio exposure category in the security policy 2026-07-26 14:24:52 +02:00
sqlc.yaml feat(shadowsaas): add shadow SaaS findings schema and review flow 2026-08-05 12:01:29 +02:00

Viezure

A portfolio exposure management platform. Viezure turns an organization's real architecture into a quantified register of what it stands to lose: every exposure priced as a band rather than a point estimate, assigned an owner, correlated with current threat intelligence, and reduced or accepted deliberately. Compliance scoring and automated control tests are inputs to that portfolio, not the frame. The positioning source of truth is docs/positioning.md.

Status: feature-complete; the hardening backlog is tracked in docs/code-audit.md. 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 & exposure trends, the Ask Melly 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

Viezure 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. chi keeps routing to stdlib net/http idioms 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. goose migrations are plain reversible SQL.
  • river jobs. 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.yaml is the single source of truth. make gen derives 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-ui primitives, and layerchart for 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. This numbered list is the canonical statement of them: code, migration, and doc comments cite it as "README.md rule N".

  1. Tenant isolation. Every tenant-owned table has org_id and 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.
  2. Module boundaries. Modules depend on each other's interfaces, never on another module's repository or tables. Only internal/platform touches infra directly.
  3. Secrets live in env/secret-manager: never in code, never sent to the frontend.
  4. LLM extraction proposes; humans dispose. Extracted assets/risks land in a review queue, never auto-committed to the register.
  5. 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, tabletop, continuity, scutty,
                       # marketplace, providers, integrations, notify/notifications, uploads,
                       # aikeys, apikeys, entitlements, ...
  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 (every internal/ package, 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 install installs Go 1.26 and Bun 1.3.14 at the versions mise.toml pins, 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 viezure-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 Lint, format-check, type-check the SvelteKit app + unit tests
make web-format Reformat the SvelteKit app with Prettier

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 the scut database; the suite runs on scut_test. Their River job queues never overlap, so a running worker can't grab and reprocess the jobs the tests enqueue. make test is safe to run alongside a live make 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_app role. Superusers bypass Row-Level Security, which would turn tenant-isolation assertions into false passes. scut_test is owned by scut_app and every tenant table is FORCE 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-lint on changed Go; bun run lint + bun run check + bun run test:unit when web/ 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 runs the frontend gate (lint, format check, type-check, unit tests) and 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:


License

No LICENSE file has been added yet; until one is, treat this repository as internal / all rights reserved.