Open source · Apache-2.0

Repository template

A SaaS security foundation for building AI systems

Multi-tenant by default, with optional single-tenant root routing. A Go core that owns facts, authority, policy and MCP; a Next.js web surface; an optional internal Python intelligence service; PostgreSQL, Redis and S3-compatible storage. Everything runs in Docker. Designed and built in 2 weeks.

You configure it and build your product on it. Its value is the reviewed security boundaries it ships and the repeatable checks behind them.


The repository is a GitHub template: files are copied as your own new repository.

Live demo. The button opens a GitHub issue that shows the demo at: https://staging.beginify.net, before you submit it; nothing is sent until you do. Demos are started by hand: Peter replies on the issue with availability, and a session usually lasts about two hours. The demo is not automatic and not always live.

Getting started

Ship with confidence

Security boundaries from Day 0.

Each card summarizes a mechanism described in the public documentation. Security statements are scoped to the tested configuration and the checks the guides name.

  1. Tenant boundary with row-level security beneath it

    The Organization is the tenant boundary, resolved on the server from the authenticated session and the request host, never from client- or model-supplied identifiers. One RoleBinding store feeds a closed, deny-by-default policy evaluator.

    PostgreSQL row-level security sits beneath core's authorization, so an omitted application predicate cannot expose another Organization or an unauthorized Group.

  2. Explicit Group authority

    Group content requires an explicit Group role. Organization owners and admins see Group metadata only; ordinary Group management by them needs the Organization owner's override, which is off by default, changed only by an owner and audited when changed or used.

    The override grants no Group content, no ownership powers and no self-join.

  3. Ownership continuity, invitations and enrollment

    A database authority guard keeps at least one owner on every Organization and Group, and ordinary admin authority cannot grant ownership to itself.

    Invitations are scoped, expire within 30 days, are revocable and are consumed once; delivery e-mail or token possession alone is not authority. New Organizations start invite-only; owners can switch to request-access or open enrollment.

  4. Host-bound sessions and revocation

    WorkOS AuthKit authenticates people; core owns the sessions. The session credential is an opaque 256-bit value of which only the SHA-256 is stored, with a 30-minute idle and 8-hour absolute lifetime by default.

    A browser session is bound to the exact origin and Organization of its sign-in and resolves nowhere else. Signing out everywhere revokes every session of the user.

  5. Tenant-scoped object storage and MCP clients

    Core derives every object key from the Organization and object IDs and authorizes each read, upload and delete against the object's recorded scope before any provider call; row-level security enforces the same table independently. Signed download URLs are GET-only, expire after at most 300 seconds and are never logged. The storage layer is core-internal: no route wires it in yet.

    An AI client connects to core over MCP through its own connection URL, bound to one Organization, one user and one client. Every call is reauthorized against the user's current membership and Group, and the member can revoke the connection.

  6. Durable audit, guarded recovery and retention

    An append-only audit relation records foundation-level changes, security denials and storage outcomes; database triggers capture row changes in the same transaction. The audit record is core-internal, with no HTTP surface or export.

    The recovery tool provides a fenced backup, restore into a fresh closed target, session invalidation before reopening, bounded retention maintenance and backup expiry, with a synthetic drill. A successful local drill is not production backup qualification.

Included development workflow

Restruct™ SDLC for agent-driven engineering management

SaaStacular includes the Restruct SDLC skill.

It is pinned as an unaltered submodule at .agents/skills/restruct-sdlc/ and keeps its own MIT license.

  • It covers the delivery steps in order: Goal, Spec, Plan, implementation, review and validation, and a Result record, with budgets, parallel scheduling, evidence and cleanup.
  • You record who decides and which roles agents play, then adapt or replace those rules. Three child skills cover adopting the workflow, supervising and reviewing, and handing work between local agent sessions.
  • It is optional. The application's build, tests, migrations and runtime do not depend on it, installing it starts no agent and creates no records, and the application's CI and local checks do not run Restruct's tests.

SaaStacular adheres to the Foundation-First rather than “Feature-First” principle of Restruct™, set out in the handbook chapter Foundation-First Development: the governed foundation (identity, tenancy, authorization, isolation and audit) ships first, and product features are yours to build on top of it.

Designed and built in 2 weeks. As a case study, this foundation represents an estimated 4+ months of conventional human development.

With well-managed, carefully architected agentic development—not vibe-coded AI slop—that upfront investment is now practical, even before the visible product features arrive.

Read the full handbook to learn more about Restruct™ ⟶

Restruct shield badge: AI-first operating methodology for modern software organizations

Architecture

A base for any SaaS. Peace-of-mind security for all your AI features, from scratch.

Every arrow below comes from the repository's README, SECURITY guide, foundation guides or MCP contract; the source for each is listed under its diagram. They show what the template does today, not a deployment you operate, and the security statements are scoped to the tested configuration.

Diagram 1 Requests, services and data access
Requests, services and data accessBrowsers reach web, which calls core. An AI client reaches core over MCP. Core reaches PostgreSQL, object storage, WorkOS AuthKit and Redis, and probes the optional intelligence service, which can call a model gateway. The text equivalent follows the image.BrowserPeople using the web app.AI clientAn external MCP clienta member connected.Model gatewayOpenRouter, only whenenabled.webThin server-side bridge.No authority or policy;no datastore, WorkOS orgateway credentials.intelligenceOptional internal AIexecution. Not reachablefrom a browser; nodata-store access.coreOwns identity and sessions, tenancy, the authorization decision, audit, storage, domains and MCP.Resolves the Organization on the server.PostgreSQLDurable state.Least-privilegeruntime role underforced row-levelsecurity.Object storageS3-compatible API(MinIO locally).Bucket-scoped user.Core-internal: noroute exposes it.WorkOS AuthKitHosted sign-inprovider.API key lives incore only.RedisEphemeral.Readiness checkonly today.HTTPS via your TLS edgeinternal URL, server-sideMCP, one URLper connection;each call isreauthorizedHTTPS, onlywhen enabledreadinessprobe onlydurable stateS3-compatible APIHTTPS APIreadinessonlyarrow: a call from source to targetaccent border: authoritydashed: optional
  • Browser → web: HTTPS through a TLS edge you provide; plain HTTP on loopback for local use only.
  • web → core: server-side requests to an internal URL. web has no authority, policy or domain rules, and no datastore, WorkOS or gateway credentials.
  • AI client → core: MCP over a per-connection URL bound to one Organization, one user and one client; each call is reauthorized. It does not pass through web.
  • core → PostgreSQL: durable state, through a least-privilege runtime role under forced row-level security.
  • core → object storage: S3-compatible API (MinIO locally) with a bucket-scoped user. Core-internal today: no route exposes it yet.
  • core → WorkOS AuthKit: HTTPS to the hosted sign-in provider; its API key lives in core only.
  • core → Redis: a readiness check only today. Redis is ephemeral.
  • core → intelligence (optional): a readiness probe only today. intelligence is not reachable from a browser and has no data-store access.
  • intelligence → model gateway (optional): HTTPS to OpenRouter, only when enabled; the key lives in intelligence only.

What has no arrow: in the shipped template the browser, web, AI clients and intelligence do not connect to PostgreSQL, Redis or object storage. Signed download URLs (GET-only, at most 300 seconds) exist in the storage layer, but no route issues them yet.

Where each arrow comes from
Diagram 2 Organization, Group and tenant authority
Organization, Group and tenant authorityAn authenticated person is resolved on the server to an Organization, the tenant boundary, which contains Organization roles, Groups and Business Profiles. Three separate authority boundaries apply inside it, enforced by the RoleBinding store, core's deny-by-default policy and PostgreSQL row-level security. The text equivalent follows the image.Authenticated personIdentity is global:WorkOS AuthKit authenticates,core owns the session.Resolved on the serverThe Organization comes from theauthenticated session and therequest host, never from a client-or model-supplied identifier.selects the tenantOrganization: the tenant boundaryOrganization rolesorg_owner, org_admin,org_member.No role is global.GroupsCollaboration scopes.Each is in exactly oneOrganization for life.Roles: group_owner,group_admin,group_member.Business ProfilesReusable context ownedby one Organization. AGroup links to at mostone profile of its ownOrganization; one profilecan be shared byseveral Groups.three authority boundaries1. Read Group metadataOrganization owners andadmins read names, slugs,onboarding state andprofile fields of everyGroup. Never rosters,invitations, tokens orobjects.2. Manage a GroupNeeds an explicit Groupowner or admin role, orthe owner-controlledoverride: off by default,audited; no ownershippowers and no self-join.3. Group contentNeeds an explicit Grouprole, whether theoverride is on or off.enforced in two layersRoleBinding storeThe only place a roleis written. Membershipsare read-only views.Policy in coreOne closed, deny-by-default policy. Everyoperation rechecks thesession, user and facts.Row-level securityIn PostgreSQL. Core setsuser, Organization andGroup as transactioncontext first. Designedso an omitted predicatecannot expose anotherOrganization.arrow: leads to the next stepaccent border: authority
  • Authenticated person: identity is global; WorkOS AuthKit authenticates and core owns the session.
  • Resolved on the server: core resolves the Organization from the authenticated session and the request host, never from a client-supplied or model-supplied identifier. An unknown or foreign Organization gets the same denial as no membership.
  • Organization, the tenant boundary, contains its roles (org_owner, org_admin, org_member; no role is global), its Groups (each in exactly one Organization for life; group_owner, group_admin, group_member) and its Business Profiles (owned by one Organization; a Group links to at most one profile of its own Organization; one profile can be shared by several Groups).
  • Three authority boundaries: (1) Organization owners and admins read Group metadata, never rosters, invitations, tokens or objects; (2) managing a Group needs an explicit Group owner or admin role, or the owner-controlled override, off by default and audited, with no ownership powers and no self-join; (3) Group content needs an explicit Group role, whether the override is on or off.
  • Enforced in two layers: the RoleBinding store (the only place a role is written), core's closed deny-by-default policy, and PostgreSQL row-level security beneath it.

What it does not show: a separate database per tenant, a browser reaching the database, or authority taken from names, slugs or model output. The template has none of these.

Where each element comes from

Included test harness

Docker tests that account for what they create.

The repository ships its own Docker integration-test and resource-lifecycle harness. infra/ci/validate.sh is the single validation entry point, locally and in CI: it runs a chosen profile with evidence scanning and owned-resource cleanup, and fails closed when the host does not meet that profile's capacity.

  • Every Docker allocation of a run with a resource registry is counted by exact ID against a finite, source-derived ceiling. A crossing or a failed Docker query refuses the allocation, creates nothing and holds the scope.
  • The migrations suite reuses one long-lived runner container for its eligible Go invocations, checking its identity, processes and file changes before and after each one. Each invocation still gets its own fresh database; other suites keep their own resources.
  • Teardown is verified by exact ID. A crash leaves the runner as a registry row for the next launch's recovery gate, and resources.tsv reports what each scope created and left over.
  • The affected profile selects only the checks a committed change requires and falls back to the full profile for unregistered paths or an unusable base. A live N/TOTAL counter tracks required checks as they run.

Compared with a resource reaper. Ryuk, the reaper in Testcontainers (source), removes the containers, networks and volumes that carry a test session's labels after a delay once its connection closes. That is resource cleanup.

This harness covers the wider validation workflow: allocation ceilings that refuse before anything is created, exact run ownership and cleanup evidence, restart recovery gates, affected-check selection and scanned validation reports. It is the repository's own tooling, not a dependency or a replacement for either, and no compatibility is claimed.

Limits. Nothing here claims a time saving or leak-free operation. The runtime added by the accounting has not been measured, and some Docker behaviours it relies on, such as event ordering and a real reboot, have not yet been observed on real Docker. Local runs need Docker Engine 24 or later with BuildKit, Compose 2.24 or later, Bash 4.4 or later, GNU coreutils, findutils and grep, plus git, setsid, ps, timeout and flock.

Screenshots

The interface, for reference

Captured from the public source running its identity test environment: the pinned WorkOS emulator with seeded synthetic Organizations and users. No real accounts or e-mail appear.

Getting started

Four commands from the repository root

We recommend using your coding-agent harness to set up, develop and operate SaaStacular. The documented commands are recipes for your agents; you are not expected to run the routine workflow by hand. You provide configuration and approve key decisions.

You need Docker Engine or Docker Desktop with Compose 2.24 or later, bash with coreutils and flock, and Git for the developer checks. No host Go, Python, Node, PostgreSQL, Redis or MinIO is needed. These are the README's quick-start commands, shown here for reading.

  1. Configure

    Creates .env from .env.example with locally generated secrets. It never overwrites an existing .env.

    infra/local/configure-env.sh
  2. Start

    The first start downloads images and compiles MinIO from source, which takes several minutes.

    docker compose --env-file .env -f infra/compose/local.yml up -d --build --wait
  3. Migrate

    Runs as the migrator role in a one-shot container.

    docker compose --env-file .env -f infra/compose/local.yml -f infra/compose/migrate.yml run --rm --build migrate status
    docker compose --env-file .env -f infra/compose/local.yml -f infra/compose/migrate.yml run --rm --build migrate up
    docker compose --env-file .env -f infra/compose/local.yml -f infra/compose/migrate.yml run --rm --build migrate status
  4. Check

    Formatting, vet, unit tests, lint, type checks and secret scanning, all in containers without network access.

    docker compose --env-file .env -f infra/compose/local.yml --profile checks run --rm --build core-check
    docker compose --env-file .env -f infra/compose/local.yml --profile checks run --rm --build web-check
    docker compose --env-file .env -f infra/compose/local.yml --profile checks run --rm --build intelligence-check
    bash infra/security/secret-scan.sh --env-file .env secret-scan

Delivery tooling. Includes GitLab CI and GCP staging and isolated preview workflows, with Terraform for the staging VM (Deployment). The modular structure can be adapted to GitHub Actions and other hosting providers by replacing the provider-specific CI and infrastructure adapters; the validation logic already runs in Docker (GitHub Actions parity guide). Included means shipped, not provisioned: you supply your own cloud project, credentials, DNS and CI environments, and this tooling has not been run against any provider for this repository. No GitHub Actions workflow is shipped, and nothing switches clouds automatically.

Portrait of Peter C. Romano

Author

Peter C. Romano

Software Engineer & Architect

Contact Me