FAQ
All answers are sourced from project_context_map.md, kernel-spec.md,
CONVENTIONS.md, and the service source code. Terms used here are
defined in the Glossary.
Getting Started
What is Platform-Kernel?
Platform-Kernel is an industry-agnostic, modular B2B2B backbone that gives independent developers seven ready-made primitives — Auth, Data, Events, Notify, Files, Money, and Audit — so they can build vertical SaaS modules without re-implementing multi-tenancy, identity, payments, or observability infrastructure.
What languages and runtimes does Platform-Kernel support?
The backend is written in Go 1.24. The SDK and UI Shell are written
in TypeScript (Node 24, pnpm 10). The sandbox runtime uses Rust/Wasm.
The monorepo is polyglot: Go workspace (go.work), pnpm workspace,
and Bazel 9.x.
What is the minimum Node.js version for the SDK?
Node 24 (Active LTS). The repository targets @types/node 25.x,
which is compatible with both Node 24 LTS and Node 25 Current.
Where do I start to build a module?
npx @platform/create-module my-module
This scaffolds a Module Federation 2.0 remote with the correct
manifest.json, Vite config, @platform/sdk-core dependency, and
permission declarations. See the
Module Development Guide.
Authentication and Identity
What JWT algorithm does the platform use?
ES256 — ECDSA with the P-256 curve and SHA-256 hash. RSA-based tokens are not accepted. Access token TTL: 15 minutes. Refresh token TTL: 7 days.
What is the password hashing algorithm?
Argon2id following NIST 800-63B: memory 64 MB, iterations 3, parallelism 4, output 32 bytes, salt 16 bytes. Minimum password length: 12 characters.
How do I enable MFA for a user?
import { useAuth } from "@platform/sdk-auth";
const { enableMFA, verifyMFA } = useAuth();
// 1. Enable — returns a QR code URI for the authenticator app
const { qrUri } = await enableMFA();
// 2. Verify the first TOTP code to confirm enrollment
await verifyMFA({ code: "123456" });
MFA follows RFC 6238 (TOTP). Users receive 10 recovery codes at enrollment (stored bcrypt-hashed). Each recovery code is one-time-use.
Can I add custom OAuth providers?
Yes. The IAM service has a pluggable provider architecture in
services/iam/internal/provider/. Register a new provider via
POST /api/v1/auth/providers. Supported types: OAuth 2.0 (Google,
GitHub, etc.), wallet, social, banking, enterprise, and custom.
What is a Delegation Token?
A short-lived JWT (default TTL: 3 600 seconds) issued by the IAM
service that lets a Platform Owner or Partner act on behalf of a
downstream client tenant. The token carries a parent_tenant_id
claim validated by the Gateway delegation middleware. Configure TTL
via DELEGATION_TOKEN_TTL.
Multi-Tenancy (B2B2B)
What does B2B2B mean in this context?
Three commercial tiers: Platform Owner (root) → Partner
(reseller/integrator) → Client (end tenant). Each tier is a
row in the tenants table connected by a closure table for O(1)
ancestry queries. Partners can provision clients and inherit
platform policies.
How is tenant data isolated?
Isolation is enforced at every layer:
- PostgreSQL — Row-Level Security per table (
SET LOCAL app.current_tenant_id). - ClickHouse — Row Policy + application-level
WHERE tenant_id = ?(dual enforcement). - Kafka — SDK-level filtering by
tenantIdfrom JWT. - WebSocket — channel namespacing
{tenantId}:{channel}. - Feature Flags — key prefix
{tenantId}:{flagName}. - S3 — path prefix
{tenantId}/{bucket}/.
How do I switch between tenants as a user?
import { useAuth } from "@platform/sdk-auth";
const { selectTenant, listTenants } = useAuth();
const tenants = await listTenants(); // GET /api/v1/tenants
await selectTenant(tenants[1].id); // POST /api/v1/tenants/select
// A new access token scoped to the selected tenant is returned
Data Primitive
What query language does the Data Layer use?
A PostgREST-inspired URL DSL:
GET /api/v1/data/{module}/{model}?status=eq.active&price=gt.100&order=price.desc&limit=20&cursor=xxx
Supported operators: eq, neq, lt, lte, gt, gte,
ilike, in, or. Relation selection: ?select=name,price,company(name).
What are the limits per module?
- Max record size: 1 MB (
DATA_MAX_RECORD_SIZE_BYTES). - Max JSONB field: 256 KB (
DATA_MAX_JSONB_SIZE_BYTES). - Max tables per module: 50 (
DATA_MAX_TABLES_PER_MODULE). - Max relation depth: 2 levels.
- Max filters per query: 10.
- Max limit per page: 100 rows.
See the full limits table in Limits Reference.
How do real-time updates work (CDC)?
Changes in PostgreSQL are captured by Debezium (WAL reader),
published to Kafka (platform.data.events), and consumed into
ClickHouse in < 5 seconds. The SDK useRealtime() hook opens a
WebSocket subscription to receive these events in the browser.
Events Primitive
What is the difference between Kafka and RabbitMQ in this platform?
Kafka (platform.{domain}.events) carries domain events — high-
throughput, durable, fan-out to multiple consumers. RabbitMQ carries
transactional tasks — lower-throughput, per-tenant queues, used
by Notify for reliable outgoing message delivery.
How do I publish a custom event from a module?
import { publish } from "@platform/sdk-events";
await publish({
topic: "platform.module.events",
type: "my-module.order.created",
entityId: order.id,
tenantId,
payload: { orderId: order.id, total: order.amount },
});
The entityId is used as the Kafka partition key.
What happens to events that cannot be delivered?
After KAFKA_MAX_ATTEMPTS retries, the event is moved to the Dead
Letter Queue (DLQ). Inspect via GET /api/v1/integrations/dlq.
Replay a single entry: POST /api/v1/integrations/dlq/:id/retry.
Notifications and WebSocket
How do I send a notification?
import { send } from "@platform/sdk-notify";
await send({
tenantId,
userId,
channel: "email",
template: "welcome",
variables: { name: "Alice" },
idempotencyKey: crypto.randomUUID(),
});
What are the rate limits for notifications?
- 100 notifications/minute per tenant (
NOTIFY_RATE_LIMIT_PER_TENANT). - 50 notifications/minute per module.
- Batch max: 500 notifications per request.
- Retry policy: 5 retries, exponential backoff 30 s → 480 s ± 10% jitter.
How do WebSocket connections work?
Connect to wss://notify.platform.io/ws. Send authentication as
the first message:
{ "type": "auth", "token": "<JWT>" }
Heartbeat: ping/pong every 30 seconds, timeout 10 seconds. On reconnect, the server replays the last 100 messages from the Valkey-backed replay buffer (TTL: 1 hour). Limits: 1 000 concurrent connections per tenant, 200 messages/second per tenant.
File Storage
What file types are supported?
All file types are accepted. Uploaded files pass through the ClamAV antivirus scanner (INSTREAM TCP). Infected files are rejected and never moved from staging to main storage.
What is the upload size limit?
- Max image size: 10 MB (
FILES_MAX_IMAGE_SIZE_MB). - There is no hard-coded global limit for non-image files; the
practical limit is set at the infrastructure level (Nginx/Envoy
client_max_body_size).
How does the upload pipeline work?
Client → POST /api/v1/files/upload (multipart)
→ staging bucket (SeaweedFS)
→ ClamAV scan (TCP INSTREAM)
→ PASS: move to main bucket
→ FAIL: reject (delete from staging, return 422)
Staging TTL: 24 hours. Maximum pending uploads per tenant: 100.
What image thumbnail presets exist?
| Preset | Dimensions |
|---|---|
icon_32 | 32 × 32 px |
avatar_64 | 64 × 64 px |
card_300 | 300 × 200 px |
preview_600 | 600 × 400 px |
full_1200 | 1200 × auto px |
Generate via:
POST /api/v1/files/:id/process or
GET /api/v1/files/:id/thumbnail/:preset.
How long are deleted files retained?
Soft-deleted files remain in the trash for 30 days
(FILES_SOFT_DELETE_RETENTION_DAYS). Use
POST /api/v1/files/:id/restore to recover before expiry.
Permanent deletion: DELETE /api/v1/files/:id/permanent.
Money and Wallets
How do I transfer funds between wallets?
import { transfer } from "@platform/sdk-money";
await transfer({
fromWalletId: "wlt_abc",
toWalletId: "wlt_xyz",
amount: 1000, // in cents
currency: "USD",
idempotencyKey: crypto.randomUUID(),
});
All transfers use ACID transactions with SELECT FOR UPDATE and
deadlock prevention (lock ordering by UUID ASC). Amounts are stored
as BIGINT (cents) — floating-point is never used.
What is the hold/confirm/cancel pattern?
- Hold:
POST /api/v1/wallets/:id/hold— reserves funds. Reducesavailable, increasesfrozen. Default TTL: 72 hours. - Confirm:
POST /api/v1/wallets/:id/confirm— converts hold into a permanent debit. - Cancel:
POST /api/v1/wallets/:id/cancel— releases the hold back toavailable.
Maximum concurrent holds per wallet: 100. Maximum hold TTL: 7 days.
What is a reversal?
A reversal undoes a completed transaction. Reversals are allowed up
to 365 days after the original transaction (MONEY_REVERSAL_MAX_AGE_DAYS).
A reversal creates two new double-entry journal entries that cancel
the originals. Reversals are not available for hold-related
transactions — use Cancel instead.
What is the maximum transaction amount?
$100 000 (10 000 000 cents) per transaction. Maximum wallet balance: $1 000 000 (100 000 000 cents) — governed by the active Billing plan. Minimum transaction: $0.01 (1 cent).
Billing and Subscriptions
What is the subscription escalation timeline?
| Stage | Duration | Effect |
|---|---|---|
| Grace period | Days 0–7 after due date | Full access, payment reminders sent |
| Suspended | Days 8–37 | Read-only access to all primitives |
| Terminated | Day 38+ | Soft-delete, data retained 90 days |
Reactivation before Day 38 restores full access immediately.
What Billing permissions exist?
billing.plan.change · billing.payment.manage ·
billing.info.update · billing.view · billing.export
Self-Hosting
What is the minimum hardware for production?
See Requirements for the full specification. Summary: 3 application nodes (4 vCPU / 8 GB RAM each), 1 PostgreSQL primary (8 vCPU / 32 GB RAM), separate nodes for Kafka, ClickHouse, and Vault HA cluster.
Can I use a managed database instead of self-hosted PostgreSQL?
Yes. Any PostgreSQL 17-compatible managed service (AWS RDS,
Google Cloud SQL, Supabase) works. The critical requirement is
that logical replication must be enabled (wal_level = logical)
for the CDC pipeline (Debezium).
How are secrets managed?
All secrets are stored in HashiCorp Vault 1.19. There is no
.env fallback for secrets (AD-1 architectural decision): if
Vault is unreachable at startup, the service exits after the
VAULT_INIT_TIMEOUT_SEC timeout (default: 120 seconds).
See Vault Setup.
Is there a Docker Compose quickstart?
git clone https://github.com/your-org/platform-kernel.git
cd platform-kernel/.dev/kernel
docker compose up -d
See Docker Compose Guide for the full walk-through and service readiness checks.
Observability and Debugging
Where do I find logs?
All 12 Go services emit structured JSON logs via slog. In Docker
Compose, use docker compose logs -f <service-name>. In Kubernetes,
use kubectl logs -f -l app=<service-name>. Every log entry
includes trace_id for correlation with OpenTelemetry traces.
How do I check service health?
Every service exposes three probes on its internal port:
GET /live— Kubernetes liveness probe (always returns200if the process is running).GET /ready— Kubernetes readiness probe (returns200only when all dependencies are connected).GET /health— Full dependency health report (PostgreSQL, Valkey, Kafka, etc.) with latency in milliseconds.
Where are metrics available?
OpenTelemetry metrics are pushed to VictoriaMetrics via the OTel
Collector (OTLP gRPC port 4317). Grafana dashboards are available
at the monitoring endpoint configured in your deployment. See
Monitoring Guide.
See Also
- Glossary — term definitions
- Migration Guides — breaking changes
- Limits Reference — all numeric limits
- Error Catalog — RFC 9457 error types
- Configuration Reference