Skip to main content

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 tenantId from 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?

PresetDimensions
icon_3232 × 32 px
avatar_6464 × 64 px
card_300300 × 200 px
preview_600600 × 400 px
full_12001200 × 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?

  1. Hold: POST /api/v1/wallets/:id/hold — reserves funds. Reduces available, increases frozen. Default TTL: 72 hours.
  2. Confirm: POST /api/v1/wallets/:id/confirm — converts hold into a permanent debit.
  3. Cancel: POST /api/v1/wallets/:id/cancel — releases the hold back to available.

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?

StageDurationEffect
Grace periodDays 0–7 after due dateFull access, payment reminders sent
SuspendedDays 8–37Read-only access to all primitives
TerminatedDay 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 returns 200 if the process is running).
  • GET /ready — Kubernetes readiness probe (returns 200 only 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