Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .env.reference
Original file line number Diff line number Diff line change
Expand Up @@ -139,17 +139,17 @@ SYSTEMPLANE_ADMIN_PATH_PREFIX=/system

# Code-only admin options — NO env-var representation:
#
# admin.WithAuthorizer(fn func(*fiber.Ctx, action string) error)
# admin.WithAuthorizer(fn func(fiber.Ctx, action string) error)
# Required to enable the three legacy global routes. Absent = 403 on every
# legacy route. Takes a function value; cannot be expressed as an env var.
#
# admin.WithTenantAuthorizer(fn func(*fiber.Ctx, action, tenantID string) error)
# admin.WithTenantAuthorizer(fn func(fiber.Ctx, action, tenantID string) error)
# Required to enable the three tenant-scoped routes. Absent = 403 on every
# tenant route (default-deny). The library does NOT silently fall back to
# WithAuthorizer for tenant routes — consumers must opt in explicitly to
# avoid silent privilege escalation. Takes a function value.
#
# admin.WithActorExtractor(fn func(*fiber.Ctx) string)
# admin.WithActorExtractor(fn func(fiber.Ctx) string)
# Customizes how the actor identity is extracted from a Fiber request for
# audit logging on writes. Takes a function value.

Expand Down
16 changes: 8 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ This file provides repository-specific guidance for coding agents working on `li

## Project snapshot

- Module: `github.com/LerianStudio/lib-systemplane`
- Module: `github.com/LerianStudio/lib-systemplane/v2`
- Language: Go
- Go version: `1.26.3` (see `go.mod`)
- Current API generation: v1.x observability migration (extracted from `lib-commons/v5`; public observability types now come from `lib-observability`)
- Current API generation: v2.x Fiber v3 stack (extracted from `lib-commons/v5`; built on `lib-commons/v6`, `lib-observability/v2`, and `gofiber/fiber/v3`)

## Primary objective for changes

Expand All @@ -21,7 +21,7 @@ This file provides repository-specific guidance for coding agents working on `li
Root (package `systemplane`):
- Small public API facade: `api_*.go` plus `doc.go`. Public types are aliases to
`internal/client` where practical so the root import path remains
`github.com/LerianStudio/lib-systemplane`.
`github.com/LerianStudio/lib-systemplane/v2`.

Subpackages:
- `admin/` — Fiber HTTP handlers for the admin surface
Expand All @@ -44,9 +44,9 @@ Scaffolding:

Lerian shared-library boundaries are now split across four libraries:

- `github.com/LerianStudio/lib-commons/v5` — non-observability shared primitives used here: `commons/tenant-manager/core`, `commons/net/http`, and `commons/backoff`.
- `github.com/LerianStudio/lib-observability` — canonical observability stack: `log`, `tracing`, redaction helpers, span helpers, telemetry lifecycle, and `runtime` panic recovery.
- `github.com/LerianStudio/lib-systemplane` — this module; runtime-mutable configuration with Postgres/MongoDB backends.
- `github.com/LerianStudio/lib-commons/v6` — non-observability shared primitives used here: `commons/tenant-manager/core`, `commons/net/http`, and `commons/backoff`.
- `github.com/LerianStudio/lib-observability/v2` — canonical observability stack: `log`, `tracing`, redaction helpers, span helpers, telemetry lifecycle, and `runtime` panic recovery.
- `github.com/LerianStudio/lib-systemplane/v2` — this module; runtime-mutable configuration with Postgres/MongoDB backends.
- `github.com/LerianStudio/lib-streaming` — tenant-scoped event streaming; do not introduce it here unless a task explicitly asks for streaming integration.

These are external module imports. Do not rewrite them to in-repo paths. Do not reintroduce observability imports from `lib-commons`; observability has moved to `lib-observability`.
Expand All @@ -58,7 +58,7 @@ These are external module imports. Do not rewrite them to in-repo paths. Do not
The Client runs in one of two modes selected at construction:

- **Single-tenant** (default). The constructor receives a non-nil `*sql.DB` / `*mongo.Client`. Reads serve from an in-process cache; writes upsert through the store and update the cache. The backend's changefeed (LISTEN/NOTIFY on Postgres, change stream on MongoDB) drives invalidation and fires `OnChange` subscribers.
- **Multi-tenant** (opt-in via `WithMultiTenantEnabled()`). DB/client may be nil. Every read/write resolves the tenant database from `ctx` via `tmcore.GetPGContext(ctx, module)` / `tmcore.GetMBContext(ctx, module)` (`lib-commons/v5/commons/tenant-manager/core`). For Postgres the lib performs NO runtime schema provisioning — `systemplane_entries` plus its NOTIFY trigger function/triggers must be created externally via `SchemaSQL()` / `DefaultSeedSQL()` (e.g. the consumer's migration pipeline); the runtime role only needs DML + `LISTEN`. (MongoDB still bootstraps its collection/indexes lazily once per resolved tenant database via a `sync.Map`-backed `sync.Once` cache.) No in-process cache. No LISTEN/NOTIFY. `OnChange` returns `ErrNotSupportedInMultiTenant`. Callers wire `tenant-manager/middleware.TenantMiddleware` (with `WithPG(...)` or `WithMB(...)` and a matching module name) before the lib's handlers.
- **Multi-tenant** (opt-in via `WithMultiTenantEnabled()`). DB/client may be nil. Every read/write resolves the tenant database from `ctx` via `tmcore.GetPGContext(ctx, module)` / `tmcore.GetMBContext(ctx, module)` (`lib-commons/v6/commons/tenant-manager/core`). For Postgres the lib performs NO runtime schema provisioning — `systemplane_entries` plus its NOTIFY trigger function/triggers must be created externally via `SchemaSQL()` / `DefaultSeedSQL()` (e.g. the consumer's migration pipeline); the runtime role only needs DML. (MongoDB still bootstraps its collection/indexes lazily once per resolved tenant database via a `sync.Map`-backed `sync.Once` cache.) No in-process cache. No LISTEN/NOTIFY. `OnChange` returns `ErrNotSupportedInMultiTenant`. Callers wire `tenant-manager/middleware.TenantMiddleware` (with `WithPG(...)` or `WithMB(...)` and a matching module name) before the lib's handlers.

### Storage shape

Expand All @@ -81,7 +81,7 @@ The Client runs in one of two modes selected at construction:
- Subscriptions: `OnChange(ns, key, fn) (unsubscribe func(), error)`. Returns `ErrNotSupportedInMultiTenant` in multi-tenant mode. Callbacks invoked serially with panic recovery via `lib-observability/runtime.RecoverAndLog`.
- Registered keys carry: default value, description, validator func, redaction policy (`RedactNone | RedactMask | RedactFull`). Options: `WithDescription`, `WithValidator`, `WithRedaction`.
- Client options: `WithLogger`, `WithTelemetry`, `WithDebounce` (default 100ms), `WithListenChannel` (Postgres default `"systemplane_changes"`), `WithTable` (Postgres default `"systemplane_entries"`), `WithCollection` (MongoDB default `"systemplane_entries"`), `WithPollInterval` (MongoDB — switches to polling), `WithMultiTenantEnabled()`, `WithModule(name)` (default `"systemplane"`).
- Admin HTTP surface (`admin` subpackage): `Mount(router, client, opts...)` registers four routes at a configurable prefix (default `/system`): `GET :prefix/:namespace`, `GET :prefix/:namespace/:key`, `PUT :prefix/:namespace/:key`, `DELETE :prefix/:namespace/:key`. Options: `WithPathPrefix`, `WithAuthorizer(fn func(*fiber.Ctx, action string) error)` — default-deny — and `WithActorExtractor`. The `action` argument is `"read"` (GET) or `"write"` (PUT/DELETE). In multi-tenant mode the caller MUST mount tenant-manager middleware before `admin.Mount` so handler `c.UserContext()` carries the resolved tenant database.
- Admin HTTP surface (`admin` subpackage): `Mount(router, client, opts...)` registers four routes at a configurable prefix (default `/system`): `GET :prefix/:namespace`, `GET :prefix/:namespace/:key`, `PUT :prefix/:namespace/:key`, `DELETE :prefix/:namespace/:key`. Options: `WithPathPrefix`, `WithAuthorizer(fn func(fiber.Ctx, action string) error)` — default-deny — and `WithActorExtractor`. The `action` argument is `"read"` (GET) or `"write"` (PUT/DELETE). In multi-tenant mode the caller MUST mount tenant-manager middleware before `admin.Mount` so handler `c.Context()` carries the resolved tenant database.
- Internal `Store` interface (`internal/store`): `Start`, `Close`, `Get`, `Set`, `Delete`, `List`, `Subscribe`. Implemented by `internal/postgres` and `internal/mongodb`. Backend-agnostic contract suite lives in `systemplanetest.Run(t, factory, RunOptions{SkipSubscribe: ...})` — multi-tenant modes pass `SkipSubscribe: true`.
- Sentinel errors: `ErrClosed`, `ErrNotStarted`, `ErrRegisterAfterStart`, `ErrUnknownKey`, `ErrValidation`, `ErrDuplicateKey`, `ErrNilContext`, `ErrNotSupportedInMultiTenant`, `ErrTenantConnectionMissing`.
- `NewForTesting(s TestStore, opts ...Option) (*Client, error)` is an explicit out-of-package test helper. Build-tag gated (`unit`/`integration`); not a promised production API.
Expand Down
40 changes: 20 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
# lib-systemplane

Dual-backend (PostgreSQL / MongoDB) hot-reload runtime configuration for Lerian services. Register operational knobs (log levels, feature flags, rate limits, circuit-breaker thresholds, worker intervals) at startup, mutate them at runtime without a pod restart, and — in single-tenant mode — subscribe to change events through a LISTEN/NOTIFY (Postgres) or change-stream (MongoDB) backed subscription. First-class support for the Lerian database-per-tenant model via the `lib-commons/v5` tenant-manager dispatch layer.
Dual-backend (PostgreSQL / MongoDB) hot-reload runtime configuration for Lerian services. Register operational knobs (log levels, feature flags, rate limits, circuit-breaker thresholds, worker intervals) at startup, mutate them at runtime without a pod restart, and — in single-tenant mode — subscribe to change events through a LISTEN/NOTIFY (Postgres) or change-stream (MongoDB) backed subscription. First-class support for the Lerian database-per-tenant model via the `lib-commons/v6` tenant-manager dispatch layer.

This library was extracted from `lib-commons/v5/commons/systemplane`. The v1 line uses `lib-observability` for logging, tracing, telemetry, redaction, and panic recovery.
This library was extracted from `lib-commons/v5/commons/systemplane`. The v2 line targets the Fiber v3 stack (`lib-commons/v6`) and uses `lib-observability/v2` for logging, tracing, telemetry, redaction, and panic recovery.

## Requirements

- Go `1.26.3` or newer
- PostgreSQL 13+ **or** MongoDB 4.4+ (replica set required for change streams; polling fallback available for standalone MongoDB)
- `github.com/LerianStudio/lib-commons/v5` for tenant-manager context, admin HTTP helpers, and backoff
- `github.com/LerianStudio/lib-observability` for logging, tracing, telemetry, redaction, and panic recovery
- `github.com/LerianStudio/lib-commons/v6` for tenant-manager context, admin HTTP helpers, and backoff
- `github.com/LerianStudio/lib-observability/v2` for logging, tracing, telemetry, redaction, and panic recovery

## Installation

```bash
go get github.com/LerianStudio/lib-systemplane
go get github.com/LerianStudio/lib-systemplane/v2
```

## Operating modes
Expand All @@ -26,7 +26,7 @@ The library supports two modes; pick at construction time:
| Single-tenant | `db *sql.DB` / `*mongo.Client` | In-process cache | Through cache + store | LISTEN/NOTIFY (Postgres) or change stream (MongoDB) |
| Multi-tenant | May be nil | Resolved per-call via tenant-manager ctx | Same | Disabled — `OnChange` returns `ErrNotSupportedInMultiTenant` |

In multi-tenant mode the library does NOT hold an in-process cache. Every `Get` reads through the resolved tenant database. The lib expects the caller to wire `lib-commons/v5/commons/tenant-manager/middleware.TenantMiddleware` with `WithPG(pgManager, "<module>")` (Postgres) or `WithMB(mongoManager, "<module>")` (MongoDB) where `<module>` matches the lib's `WithModule(...)` option (default `"systemplane"`). The middleware populates the request context; the lib calls `tmcore.GetPGContext` / `tmcore.GetMBContext` to resolve the tenant database and runs the read/write against that handle.
In multi-tenant mode the library does NOT hold an in-process cache. Every `Get` reads through the resolved tenant database. The lib expects the caller to wire `lib-commons/v6/commons/tenant-manager/middleware.TenantMiddleware` with `WithPG(pgManager, "<module>")` (Postgres) or `WithMB(mongoManager, "<module>")` (MongoDB) where `<module>` matches the lib's `WithModule(...)` option (default `"systemplane"`). The middleware populates the request context; the lib calls `tmcore.GetPGContext` / `tmcore.GetMBContext` to resolve the tenant database and runs the read/write against that handle.

> **Provisioning (Postgres).** The library no longer creates its schema or seeds defaults at runtime. Provision `systemplane_entries` (plus the `systemplane_notify_v3()` trigger function and the NOTIFY triggers) and any defaults externally — e.g. via your migration pipeline — using the DDL published by [`SchemaSQL()` / `DefaultSeedSQL()`](#schema-provisioning). The runtime database role only needs DML (`SELECT`/`INSERT`/`UPDATE`/`DELETE`) + `LISTEN`; it does NOT need `CREATE` on the schema.

Expand Down Expand Up @@ -61,7 +61,7 @@ import (
"os"

_ "github.com/jackc/pgx/v5/stdlib"
systemplane "github.com/LerianStudio/lib-systemplane"
systemplane "github.com/LerianStudio/lib-systemplane/v2"
)

func main() {
Expand Down Expand Up @@ -130,7 +130,7 @@ import (
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"

systemplane "github.com/LerianStudio/lib-systemplane"
systemplane "github.com/LerianStudio/lib-systemplane/v2"
)

func main() {
Expand Down Expand Up @@ -186,10 +186,10 @@ import (
"fmt"
"os"

tmpostgres "github.com/LerianStudio/lib-commons/v5/commons/tenant-manager/postgres"
tmmiddleware "github.com/LerianStudio/lib-commons/v5/commons/tenant-manager/middleware"
systemplane "github.com/LerianStudio/lib-systemplane"
"github.com/gofiber/fiber/v2"
tmpostgres "github.com/LerianStudio/lib-commons/v6/commons/tenant-manager/postgres"
tmmiddleware "github.com/LerianStudio/lib-commons/v6/commons/tenant-manager/middleware"
systemplane "github.com/LerianStudio/lib-systemplane/v2"
"github.com/gofiber/fiber/v3"
)

func main() {
Expand Down Expand Up @@ -234,8 +234,8 @@ func run() error {
tmmiddleware.WithPG(pgManager, "systemplane"),
))

app.Get("/log-level", func(c *fiber.Ctx) error {
level, _, err := client.GetString(c.UserContext(), "global", "log.level")
app.Get("/log-level", func(c fiber.Ctx) error {
level, _, err := client.GetString(c.Context(), "global", "log.level")
if err != nil {
return err
}
Expand All @@ -255,10 +255,10 @@ package main
import (
"context"

tmmongo "github.com/LerianStudio/lib-commons/v5/commons/tenant-manager/mongo"
tmmiddleware "github.com/LerianStudio/lib-commons/v5/commons/tenant-manager/middleware"
systemplane "github.com/LerianStudio/lib-systemplane"
"github.com/gofiber/fiber/v2"
tmmongo "github.com/LerianStudio/lib-commons/v6/commons/tenant-manager/mongo"
tmmiddleware "github.com/LerianStudio/lib-commons/v6/commons/tenant-manager/middleware"
systemplane "github.com/LerianStudio/lib-systemplane/v2"
"github.com/gofiber/fiber/v3"
)

func run() error {
Expand Down Expand Up @@ -303,7 +303,7 @@ In multi-tenant mode the lib does NOT provision schema at runtime for Postgres
Mount the Fiber admin surface under a configurable path prefix (default `/system`):

```go
import "github.com/LerianStudio/lib-systemplane/admin"
import "github.com/LerianStudio/lib-systemplane/v2/admin"

admin.Mount(app, client,
admin.WithPathPrefix("/system"),
Expand All @@ -320,7 +320,7 @@ PUT /<prefix>/:namespace/:key - write a single entry
DELETE /<prefix>/:namespace/:key - delete a single entry
```

In multi-tenant mode, authenticate before tenant resolution, then mount the tenant-manager middleware BEFORE `admin.Mount` so handler `c.UserContext()` carries the tenant database.
In multi-tenant mode, authenticate before tenant resolution, then mount the tenant-manager middleware BEFORE `admin.Mount` so handler `c.Context()` carries the tenant database.

### Catalog routes

Expand Down
Loading
Loading