Skip to content

PlatformKit

A runnable, open-source Go foundation for multi-tenant SaaS. Clone it, run one command, and get tenant isolation, users, authentication, API keys, audit, content, notifications, tenant branding, health, and a real operator console in one process.

Go Security License: Apache-2.0 Go 1.26+

PlatformKit — pk-core contracts and pk-design tokens feed ten reference modules, which pk-apps composes into the starter the platformkit front door runs

Run the stable starter

No clone required. @latest resolves the newest release through the Go module proxy, so this command does not go stale:

go run github.com/septagon-oss/platformkit@latest

Clone instead when you want the source to read, extend, or verify:

git clone https://github.com/septagon-oss/platformkit
cd platformkit
go run .

On a fresh database the default is deliberately predictable: the ten-module starter from pk-apps, SQLite, and a loopback-only listener. Point database.driver at postgres when you are ready to deploy.

============================================================
 PlatformKit OSS
  listening:    http://127.0.0.1:8080
  admin UI:     http://127.0.0.1:8080/admin
  health:       http://127.0.0.1:8080/healthz
  OpenAPI:      http://127.0.0.1:8080/openapi/extensions.json
  local tenant: tenant_local
  local login:  operator@local.test / local-development-only
  modules:      10 composed (...)
============================================================

Open http://127.0.0.1:8080/. The public landing page explains the running surface without leaking credentials. The terminal prints the development login; /admin presents a responsive, scope-protected operator workspace with typed forms, useful tables, lifecycle actions, empty/error states, and mobile navigation.

The fresh-database local development bootstrap is:

  • Tenant: tenant_local
  • Email: operator@local.test
  • Password: local-development-only

An upgraded database may retain previously released tenant and user IDs so downstream module rows are not orphaned. Its startup banner prints the actual tenant ID and resolved email to use; the visible labels and development password are still neutralized.

Those credentials are for local development. A configured or non-development deployment fails closed without seed.admin_password, never reasserts a changed production password, and never prints that password. PORT=8090 go run . changes the port while staying on loopback. Listening on a network interface requires an explicit address such as PK_HTTP_ADDR=0.0.0.0:8080 go run ..

The CLI

The binary is a small cobra CLI. Running with no subcommand serves, so the quickstart above never changes. Configuration precedence, lowest to highest: built-in defaults → config.yaml → environment variables → flags.

go run github.com/septagon-oss/platformkit@latest --port 9090   # loopback port
go run github.com/septagon-oss/platformkit@latest new app acme  # scaffold your own application
go run github.com/septagon-oss/platformkit@latest config init   # commented config.yaml template
go run github.com/septagon-oss/platformkit@latest version --json
go run github.com/septagon-oss/platformkit@latest modules --json
go run github.com/septagon-oss/platformkit@latest openapi > openapi.json

modules and openapi compose the real application against a throwaway in-memory database, so they never create or migrate ./pk.db. Serve flags: --addr, --port, --config, --env, --db-dsn, --admin-email, and --admin-password (prefer the PK_ADMIN_PASSWORD environment variable so the secret stays out of the process list). platformkit --help documents all of it, including the environment variables.

Make it your product

new app writes a Go application that boots this starter and is ready for your own modules; new module adds one.

platformkit new app acme && cd acme
platformkit new module invoice
make verify        # go vet + go test -race, including the generated module's tests
go run .           # your app, your name on the console

A scaffolded application carries a container image, a Makefile whose verify target is the same gate this project holds itself to, a config.example.yaml that keeps secrets in the environment, and an agent pack (AGENTS.md, llms.txt) that teaches an AI coding agent the rules for extending it safely.

Generated modules register themselves, so adding one never edits main.go. Each ships tenant-scoped queries, per-route scope checks, canonical entity IDs, append-only migrations, and a test that fails the moment tenant isolation breaks — the same contract a hand-written module must meet.

Choose a database

SQLite is the default and needs no setup. For a real deployment, point the driver at Postgres:

database:
  driver: postgres
  dsn: "postgres://user:pass@host:5432/db?sslmode=require"

The binary registers both drivers, so the engine is configuration, not a rebuild. Nine of the ten module stores have a real Postgres adapter, and both adapter sets pass the same store conformance suite, so a missing tenant predicate fails a test on either engine. The exception is the new branding module: its Postgres adapter has not landed yet, so a Postgres deployment composes nine modules (stock chrome) and refuses to boot if branding seed values are configured — the gap is loud, not silent. Supply the administrator password through PK_ADMIN_PASSWORD; it is applied after config.yaml loads, so the secret stays out of your config, git history, and image layers.

Use the API

Authentication resolves a server-owned tenant and subject. Built-in resources require explicit <resource>:read or <resource>:write scopes; the seeded administrator has full access. API keys cannot acquire interactive admin or console:access capabilities.

# Use the exact values printed in this database's startup banner. These are the
# fresh-database defaults; an upgraded database can retain an older tenant ID.
TENANT_ID=tenant_local
ADMIN_EMAIL=operator@local.test
ADMIN_PASSWORD=local-development-only

# Log in. The response contains a session ID.
curl -s -X POST http://127.0.0.1:8080/api/v1/auth/sessions \
  -H 'Content-Type: application/json' \
  -d "{\"tenant_id\":\"$TENANT_ID\",\"email\":\"$ADMIN_EMAIL\",\"password\":\"$ADMIN_PASSWORD\"}"

# Send that ID as a bearer token.
curl -s http://127.0.0.1:8080/api/v1/tenants \
  -H 'Authorization: Bearer YOUR_SESSION_ID'

Useful runtime routes:

Route Purpose Access
/ Product and runtime landing page Public
/admin Schema-aware operator console admin + console:access
/healthz, /live, /ready Health and orchestration probes Public
/metrics expvar process and module metrics metrics:read or admin
/openapi/extensions.json Validated OpenAPI 3.1 extension operations Public
/api/v1/tenants Read/update/delete the caller's tenant; provisioning is out of band tenants:read/write or admin
/api/v1/users User management users:read/write or admin
/api/v1/api-keys Scoped machine credentials api-keys:read/write or admin
/api/v1/audit-events Append-only audit query audit:read or admin
/api/v1/content Stored content and publish lifecycle content:read/write or admin
/api/v1/notifications Stored in-app notification records notifications:read/write or admin

All request bodies are capped at 1 MiB. Mutating JSON rejects unknown fields and trailing values, malformed or negative pagination returns 400, and API key scopes must be built in or declared by an application module. Anonymous mutations are rejected, tenant identity comes from the verified credential rather than request JSON, and process metrics are not public.

The generic starter does not invent end-user presentation. Creating a notification persists a user-scoped record for the API/operator surface; it does not display a navbar bell, toast, email, SMS, or push message. Creating or publishing content persists its lifecycle state; it does not create a public page, template, or URL. Those delivery and rendering choices belong in the downstream application. See the current runtime boundaries.

Build your product on top

This repository stays deliberately domain-neutral. Product-specific modules belong in your application repository, where they can be changed or replaced without forking PlatformKit.

The supported starterapp.WithModules seam composes application-owned modules into the same SQLite pool, module catalog, identity perimeter, admin and health registrars, request limits, and OpenAPI discovery as the built-ins:

err := starterapp.Run(
    ctx,
    starterapp.DefaultConfig(),
    starterapp.WithModules(yourModule),
)

Start with the generic pk-apps/reference/custommodule reference, then keep your domain model, migrations, routes, and policies in the repository that owns the product. The foundation remains reusable whether the result is a CRM, marketplace, internal tool, booking system, or another SaaS. The reference declares application API-key scopes, enforces them on every route, uses append-only embedded migrations, derives identity from the authenticated principal, and tests tenant isolation and strict inputs.

When you need to see the seam carry a full domain rather than a minimal one, pk-apps/reference/polls adds a lifecycle, an audit outbox committed atomically with each mutation, signed anonymous voter identity, per-network throttling, /metrics counters, and a public browser surface beside the JSON API.

What is included

  • Tenants — isolation in stores and request identity.
  • Users — tenant-scoped records and password lifecycle.
  • Authentication — browser sessions and bearer-session support.
  • API keys — one-time plaintext display and explicit machine scopes.
  • Audit — append-only operational events.
  • Content — draft and publish lifecycle.
  • Notifications — tenant/user-scoped in-app messages.
  • Branding — tenant logo and palette with WCAG-corrected derivation; themed chrome and first-login setup.
  • Admin — a responsive, schema-aware reference console.
  • Health — module health plus runtime liveness/readiness.

Verify before shipping

make verify        # format, vet, staticcheck, tests, race, and build
make coverage      # atomic coverage profile and function report
make security      # govulncheck + gosec
make release-check # all of the above

GitHub Actions runs the verification and coverage gate on every pull request, plus dependency review, CodeQL, govulncheck, and gosec. The module has no local replace directives, so a clean clone exercises the same public dependency graph a user gets.

Boundaries and expectations

  • This is not a no-code product. Extension code is Go.
  • It is not a Rails/Django-style MVC framework or ORM.
  • SQLite is the zero-setup default for local development and small single-node deployments; set database.driver: postgres for the production profile. Both adapter sets pass the same store conformance suite, so the engine is a configuration choice, not a rewrite.
  • The reference admin is a useful operator surface, not an enterprise policy engine.
  • PlatformKit is pre-1.0. Pin versions and expect deliberate API evolution.
  • Modules are optional. Take the starter, select another composition, or build your own through the same ports.
  • Notification email/SMS/push delivery, a navbar inbox/toast UI, and public content rendering are downstream features, not hidden starter behavior.
  • The frontend stack is Go end to end. pk-design publishes the canonical theme, tw compiles typed utility classes and emits their CSS, and pk-ui renders accessible components — no Node, no Tailwind build, no bundler. There is still deliberately no Storybook gallery or Figma export here; those belong to downstream distributions.
  • Schemas are Go modules compiled into the binary, not collections defined at runtime through the admin UI. If you want to add a field by clicking in a dashboard, a runtime-collection backend such as PocketBase or Directus fits better. PlatformKit trades that for multi-tenancy, scoped machine credentials, an append-only audit trail, and module contracts the compiler checks.
  • Content is stored and administered, not published. The built-in content module gives you a tenant-scoped store, an API, and an operator console; it serves no visitor-facing page. Public rendering is a downstream concern — pk-apps/reference/polls shows a module serving its own public page.

How the pieces fit

pk-core defines module and security contracts. pk-modules implements the reference capabilities. pk-apps/pkg/starterapp owns the one canonical starter composition. This repository is the domain-neutral public front door.

Modules depend on published interfaces, not one another's concrete implementations. Downstream products extend the starter through published contracts without placing their domain code in PlatformKit:

flowchart LR
  core["pk-core\nmodule + identity contracts"]
  modules["pk-modules\n10 reference modules"]
  starter["pk-apps/starterapp\nstable composition"]
  front["go run .\ndomain-neutral front door"]
  product["your product repository\napplication-owned modules"]

  core --> modules
  modules --> starter
  starter --> front
  starter --> product
  core -. "published contracts" .-> product
Loading

Static architecture asset: docs/architecture.svg

Repository family

Everything is pre-1.0: pin versions and expect deliberate API evolution. The tiers say how each repository moves, not how finished it is. Released set repositories move together — this front door pins an exact set that is boot-tested as a whole, and their tags are cut in dependency order. Toolchain repositories gate and generate code but are not linked into your binary. Foundations move fastest; consume them through the released set unless you are extending the design system itself.

Repository Purpose Tier
platformkit (this repo) Domain-neutral front door Released set
pk-apps Canonical starterapp composition library and extension seam Released set
pk-modules Reference business modules and admin Released set
pk-core Module, dependency, identity, and runtime contracts Released set
pk-shared Cross-repository vocabulary Released set
pk-runtime Hosting and health primitives Released set
pk-guard Composable go/analysis guardrails; the modules' verify gate Toolchain
pk-tools Developer tooling — pk new module, pk explain Toolchain
pk-testkit Conformance and flow testing Toolchain
pk-design Design tokens, component contracts, WCAG contrast machinery Foundations
pk-ui Component contracts, ARIA builder, and gomponents renderers Foundations
tw Typed utility-class DSL and CSS emission for the design system Foundations
styleengine Typed Go-native CSS construction, parsing, and sanitizing Foundations
pk-client Client primitives for calling a PlatformKit API Foundations
pk-docs Public architecture and operating guides — published site Docs

Project links

Releases

Packages

Used by

Contributors

Languages