lowcode-database extends the Postgres-style core with a four-tier plugin model aligned with Supabase primitives:
| Tier | Supabase / Postgres analogue | lowcode-database | Manifest key |
|---|---|---|---|
| 1 | Database Webhooks / change subscription (not Realtime WebSocket) | Same event.Emit path as lc_event_sinks; WASM plugin handlers |
subscriptions |
| 2 | Database Trigger | before / after row mutation hooks |
triggers |
| 3 | Database Function (RPC) | POST/GET /v1/data/rpc/{name} |
rpc |
| 4 | Custom Postgres TYPE | Column types plugin.{name}.{type} |
types |
Plugins run in a WASM sandbox (wazero) or as native handlers (builtins / tests). All tiers share one JSON dispatch protocol (internal/plugin/types.go, pkg/pluginsdk).
┌─────────────────────────────────────┐
│ HTTP API (chi) │
│ /v1/admin/plugins (CRUD) │
│ /v1/data/rpc/{name} (Tier 3) │
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ LowcodeService / shared.Base │
│ EmitEvent → event.Bus │
└──────────────┬──────────────────────┘
│ async deliver (same envelope)
┌───────────────────────┴───────────────────────┐
│ │
┌──────▼──────┐ ┌───────▼────────┐
│ lc_event_ │ │ plugin.Manager │
│ sinks │ │ (Tier 1 subs) │
│ HTTP/Kafka… │ │ WASM/native │
└─────────────┘ └────────────────┘
│
┌──────▼────────┐
│ data.Data │ Tier 2 triggers (before/after row SQL)
└───────────────┘
See also examples/plugins/EXAMPLE.md for copy-paste manifests, curl, and handler source.
Migration: docker/postgres/migrations/meta/000001_init.up.sql (lc_plugins, lc_plugin_kv, lc_plugin_invocations)
| Table | Purpose |
|---|---|
lc_plugins |
Plugin registry: wasm_module, manifest, config, per tenant |
lc_plugin_kv |
Plugin-private JSON KV (future host import) |
lc_plugin_invocations |
Optional audit log (PLUGIN_LOG_INVOCATIONS=true) |
{
"subscriptions": [
{ "events": ["records.after.insert"], "tableFilter": "orders" }
],
"triggers": [
{ "table": "orders", "timing": "before", "operations": ["insert", "update"] }
],
"rpc": [
{ "name": "hello", "method": "POST" }
],
"types": [
{ "id": "money", "name": "money", "pgType": "numeric", "description": "Validated money column" }
]
}Registered column type id: plugin.{pluginName}.money → e.g. plugin.demo.money.
Concept: Like Postgres change notification or Supabase Database Webhooks — subscribe to row/schema changes and react. This is not Supabase Realtime (browser WebSocket); it is the same class of problem as NOTIFY + HTTP webhook on commit.
Flow:
row/schema change → shared.EmitEvent → event.Bus.Emit (async)
├─ lc_event_sinks (external URL)
└─ plugin subscriptions (WASM in-process)
Both use the identical JSON envelope from pkg/eventschema (type, tenantId, tableId, occurredAt, data).
| Delivery | Config | Use case |
|---|---|---|
| event-sinks | POST /v1/admin/event-sinks |
Push to HTTPS, Kafka, Redis, … |
plugin subscriptions |
manifest.subscriptions |
Custom logic in WASM (index, transform, fan-out) |
Plugin subscriptions run in the same async goroutine as sink delivery (event.Bus.deliver), not on the HTTP request thread.
When: Before/after CreateRow, UpdateRow (bulk/saveGraph: planned extension).
| Timing | Behavior |
|---|---|
before |
May mutate cells or deny write (deny: true) |
after |
Side effects; errors logged, do not roll back SQL |
Demo: native plugin demo uppercases name on insert/update.
Supabase-style call:
POST /v1/data/rpc/hello
X-Tenant-Id: default
Content-Type: application/json
{"orderId": "..."}List functions: GET /v1/data/rpc.
Admin install:
POST /v1/admin/plugins
{
"name": "demo",
"nativeBuiltin": true,
"enabled": true
}(nativeBuiltin: true + name: demo loads the reference manifest from internal/plugin/builtin.)
- Plugin declares
types[]in manifest. - On reload, types register in
columntypeasplugin.{name}.{type}. GET /v1/admin/typesincludes plugin types.- Create column with
typeId: "plugin.demo.money". - Host calls plugin on
validate/toPG/fromPGduring row writes.
Guest module exports:
memorydispatch(i32 ptr, i32 len) i64— returns(outLen << 32) | outPtr- optional
malloc/free
Input/output: JSON DispatchRequest / DispatchResponse.
Author with pkg/pluginsdk; build with TinyGo or GOOS=wasip1 GOARCH=wasm.
Upload via POST /v1/admin/plugins with wasmBase64.
| Method | Path | Description |
|---|---|---|
| GET | /v1/admin/plugins |
List plugins |
| POST | /v1/admin/plugins |
Install |
| GET | /v1/admin/plugins/{id} |
Get |
| PATCH | /v1/admin/plugins/{id} |
Update manifest/config/wasm |
| DELETE | /v1/admin/plugins/{id} |
Uninstall |
| GET | /v1/admin/plugin-invocations |
Audit log |
| Env | Default | Description |
|---|---|---|
PLUGIN_LOG_INVOCATIONS |
true |
Write lc_plugin_invocations |
internal/plugin/ Runtime: Manager, WASM, native drivers
internal/plugin/builtin/ Reference native plugin "demo"
pkg/pluginsdk/ Author SDK + JSON protocol types
internal/service/platform/plugin.go CRUD
internal/service/data/row.go Tier 2 hooks (insert/update)
internal/service/shared/events.go Tier 1 fan-out
internal/columntype/types.go Tier 4 registry
- Hook bulk/saveGraph/import — extend Tier 2 to
BulkUpsertRows,SaveGraph,ImportRows. - Subscription async pool — run Tier 1 in worker pool with timeout; avoid blocking EmitEvent path.
- Trigger timeout — cap WASM execution (e.g. 50ms before-write); configurable per plugin.
- Host imports for KV/Log — expose
lc_plugin_kvand structured log to WASM (like Paca SDK). - Capability manifest — declare allowed actions; reject undeclared manifest entries at install.
- Transactional triggers (optional) —
before commitinside tx for strict invariants (higher risk). - Browser Realtime (optional, separate) — WebSocket layer for clients; not Tier 1 database subscriptions.
- Plugin marketplace — signed WASM bundles, version pinning, rollback.
- Tier 4 read path —
fromPG/computeon ListRows for display formatting. - Row-level auth plugin — authz driver delegating to WASM
authorizehook.
- PG extension bridge — optional sidecar for true PL/pgSQL functions where WASM is insufficient.
- Multi-tenant plugin isolation — separate wazero runtimes per tenant; memory limits.
- Observability — Prometheus metrics:
plugin_dispatch_seconds,plugin_denies_total.
- Event envelope:
pkg/eventschema/README.md - Event sinks (external):
docs/event-delivery.md - Auth:
internal/platform/authz/