Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 + `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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

### 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