Kalam Functions HTTP API & Routine Security
September 2, 2026 · View on GitHub
Normative companion to
functions.mdandfunctions-v1-implementation.md.
Status: Proposed for V1
Goal: Make every Kalam procedure usable as a typed REST endpoint while preserving PostgreSQL-like routine privileges, RLS, and transaction semantics.
1. One procedure, multiple invocation transports
A procedure is defined once in SQL and implemented once in the Kalam Functions runtime.
It may be invoked through:
SQL / PGWire
CALL api.create_order(...)
HTTP
POST /v1/functions/api/create_order
Nested
ctx.functions.api.createOrder(...)
Table trigger
AFTER INSERT/UPDATE/DELETE -> procedure
Topic trigger
topic message -> procedure
Scheduler
scheduled occurrence -> procedure
There is no separate REST controller definition.
The SQL procedure remains the authoritative contract for:
name
parameters
parameter types
return type
security mode
EXECUTE permissions
2. Canonical REST route
Every procedure is automatically addressable through:
POST /v1/functions/{schema}/{procedure}
Example:
POST /v1/functions/api/create_order
The route resolves directly to:
CALL api.create_order(...);
The HTTP adapter must use the same:
ProcedureRegistry
ContractSnapshot
argument binder
EXECUTE authorization
routine security mode
ExecutionContext
transaction machinery
runtime
return validation
as SQL/PGWire invocation.
Do not create a second HTTP function registry.
3. Request binding
Example SQL contract:
CREATE TYPE api.create_order_request AS (
product_id TEXT NOT NULL,
quantity INT NOT NULL
);
CREATE PROCEDURE api.create_order(
request api.create_order_request
)
RETURNS api.create_order_result
SECURITY DEFINER;
HTTP:
POST /v1/functions/api/create_order
Authorization: Bearer ...
Content-Type: application/json
X-Client-Version: 4.1
{
"request": {
"product_id": "p123",
"quantity": 2
}
}
The HTTP JSON body is only the edge representation of SQL arguments.
Internally the request is converted directly into the typed Kalam/DataFusion value model using the procedure signature.
Do not use JSON as the internal procedure object model.
Validation errors should use stable KalamDB/PostgreSQL-style errors for:
missing parameter
unknown parameter
wrong SQL type
missing required struct field
unknown struct field
nullability violation
NONEMPTY violation
payload too large
4. HTTP request context
An HTTP-originated root execution exposes request metadata through ctx.http.
Do not expose the raw Actix request type to user code.
Use a stable Kalam-owned API:
interface ProcedureHttpContext {
request: {
method: string;
path: string;
headers: ReadonlyHeaders;
query: ReadonlyQuery;
cookies: ReadonlyCookies;
};
response: {
status(code: number): void;
header(name: string, value: string): void;
contentType(value: string): void;
};
}
Example:
export default defineProcedure(async (ctx, input) => {
const version = ctx.http?.request.headers.get("x-client-version");
const language = ctx.http?.request.headers.get("accept-language");
// business logic
});
ctx.http values:
HTTP REST root -> present
SQL / PGWire root -> null
scheduler root -> null
table trigger root -> null
topic trigger root -> null
Nested procedures originating from an HTTP root may read the inherited request metadata.
Only the root HTTP procedure may mutate HTTP response metadata.
5. HTTP response contract
The declared SQL RETURNS type remains the response body contract.
Do not introduce a second HttpResponse<T> SQL type merely for REST.
Example:
CREATE TYPE api.create_order_result AS (
order_id TEXT NOT NULL,
status TEXT NOT NULL
);
CREATE PROCEDURE api.create_order(...)
RETURNS api.create_order_result;
Procedure:
export default defineProcedure(async (ctx, input) => {
const order = await ctx.db.app.orders.insert(/* ... */);
ctx.http?.response.status(201);
ctx.http?.response.header("Location", `/orders/${order.id}`);
return {
orderId: order.id,
status: "created",
};
});
Response:
HTTP/1.1 201 Created
Content-Type: application/json
Location: /orders/o456
{
"order_id": "o456",
"status": "created"
}
Default edge mapping:
composite / row -> application/json
SETOF -> JSON array
JSON / JSONB -> application/json
TEXT -> JSON scalar by default, or explicit text content type
BYTES -> binary content when explicitly selected
VOID -> successful empty response
The procedure may change:
HTTP status
response headers
content type
without changing the SQL return contract.
6. Routine access uses EXECUTE privileges
RLS is not the mechanism for deciding whether a caller may invoke a procedure.
Use PostgreSQL-like routine privileges:
REVOKE EXECUTE ON PROCEDURE api.create_order FROM PUBLIC;
GRANT EXECUTE ON PROCEDURE api.create_order TO user;
Authorization layers remain distinct:
Routine access
GRANT/REVOKE EXECUTE
Table/column access
GRANT/REVOKE SELECT/INSERT/UPDATE/DELETE
Row access
RLS / CREATE POLICY
Internally KalamDB may share one authorization engine, but the SQL surface remains PostgreSQL-like.
7. Recommended default
Internet-facing procedures should not accidentally become public endpoints.
Recommended KalamDB default:
Newly created application procedures do not grant
EXECUTEtoPUBLICautomatically.
A public endpoint must be explicit:
GRANT EXECUTE ON PROCEDURE api.health TO PUBLIC;
A user endpoint:
REVOKE EXECUTE ON PROCEDURE api.create_order FROM PUBLIC;
GRANT EXECUTE ON PROCEDURE api.create_order TO user;
This intentionally favors secure-by-default API exposure even if PostgreSQL deployments may use different default ACL behavior.
8. SECURITY INVOKER
Default:
CREATE PROCEDURE api.get_my_orders(...)
RETURNS SETOF app.orders
SECURITY INVOKER;
or simply omit the clause:
CREATE PROCEDURE api.get_my_orders(...)
RETURNS SETOF app.orders;
Semantics:
actor = authenticated caller
execution principal = caller
Inside the procedure:
table privileges
column privileges
RLS
CURRENT_USER
are evaluated using the caller's effective principal.
Use SECURITY INVOKER when the procedure should obey the caller's ordinary DB permissions and RLS.
9. SECURITY DEFINER
KalamDB V1 supports:
CREATE PROCEDURE api.create_order(...)
RETURNS api.create_order_result
SECURITY DEFINER;
Semantics:
actor = authenticated caller
execution principal = procedure owner
The caller still needs:
GRANT EXECUTE ON PROCEDURE api.create_order TO user;
but does not necessarily need direct table write privileges.
Example:
REVOKE INSERT ON TABLE app.orders FROM user;
REVOKE EXECUTE ON PROCEDURE api.create_order FROM PUBLIC;
GRANT EXECUTE ON PROCEDURE api.create_order TO user;
api.create_order can insert into app.orders only if the procedure owner's effective permissions allow it.
This makes the procedure a real application/API security boundary.
10. Actor vs execution principal
Keep these identities separate.
Actor
Who initiated the root execution.
Examples:
HTTP authenticated user
PGWire user
original actor copied into a topic event
scheduler/service actor where configured
Actor is immutable audit metadata.
Effective execution principal
Whose DB permissions are active for the current procedure frame.
For:
SECURITY INVOKER -> caller frame's principal
SECURITY DEFINER -> callee procedure owner
Conceptual context:
interface ProcedureContext {
actor: Actor | null;
principal: Principal;
// ...
}
Authorization and RLS use:
ctx.principal
Business/audit logic that needs the original user uses:
ctx.actor
Never silently replace actor identity when entering a definer procedure.
11. Nested procedure security
Routine security is evaluated per procedure frame.
Example:
user
↓
procedure A SECURITY INVOKER
principal = user
↓
procedure B SECURITY DEFINER
principal = owner_B
↓
procedure C SECURITY INVOKER
principal = owner_B
↓
return B
principal restored to user
Rules:
- nested
SECURITY INVOKERinherits the effective principal of its caller frame; - nested
SECURITY DEFINERswitches to the callee owner; - returning restores the previous frame's effective principal;
- actor remains unchanged through the root execution;
- transaction, request metadata, cancellation, revision, and source remain shared.
Do not create a second transaction or root context during a security transition.
12. RLS interaction
RLS continues to operate normally against the effective principal.
For invoker:
user -> procedure -> query table
-> RLS evaluates as user
For definer:
user -> procedure -> query table
-> RLS/authorization evaluates using procedure owner's effective principal
Any special owner/RLS bypass semantics must be defined explicitly by KalamDB's existing RLS model; SECURITY DEFINER itself should not invent an extra bypass flag.
The central rule is:
Routine security selects the effective principal; the normal authorization/RLS engine then evaluates access for that principal.
13. End-to-end example
SQL:
CREATE TYPE api.create_order_request AS (
product_id TEXT NOT NULL,
quantity INT NOT NULL
);
CREATE TYPE api.create_order_result AS (
order_id TEXT NOT NULL,
status TEXT NOT NULL
);
CREATE PROCEDURE api.create_order(
request api.create_order_request
)
RETURNS api.create_order_result
SECURITY DEFINER;
REVOKE INSERT ON TABLE app.orders FROM user;
REVOKE EXECUTE ON PROCEDURE api.create_order FROM PUBLIC;
GRANT EXECUTE ON PROCEDURE api.create_order TO user;
TypeScript:
export default defineProcedure(async (ctx, input) => {
const clientVersion =
ctx.http?.request.headers.get("x-client-version");
const order = await ctx.db.app.orders.insert({
productId: input.request.productId,
quantity: input.request.quantity,
createdBy: ctx.actor!.id,
});
ctx.http?.response.status(201);
ctx.http?.response.header("Location", `/orders/${order.id}`);
return {
orderId: order.id,
status: "created",
};
});
HTTP:
POST /v1/functions/api/create_order
Authorization: Bearer ...
X-Client-Version: 4.1
Content-Type: application/json
{
"request": {
"product_id": "p123",
"quantity": 2
}
}
Response:
HTTP/1.1 201 Created
Content-Type: application/json
Location: /orders/o456
{
"order_id": "o456",
"status": "created"
}
14. Error behavior
No execute permission:
ERROR: permission denied for procedure api.create_order
Unknown procedure:
ERROR: procedure api.create_order does not exist
Invalid REST argument:
ERROR: invalid argument for procedure api.create_order
DETAIL: field request.quantity expects INT but received TEXT.
Definer owner missing/invalid:
ERROR: cannot execute SECURITY DEFINER procedure api.create_order
DETAIL: procedure owner is not a valid execution principal.
HTTP maps stable KalamDB error codes into appropriate 4xx/5xx statuses without losing the underlying SQL-style error identity.
Do not leak internal stack traces, authorization internals, bearer tokens, cookies, or sensitive headers.
15. Catalog requirements
system.routines must include at minimum:
procedure_id
schema_name
procedure_name
owner_id
security_mode invoker | definer
return_type
active_revision
system.routine_grants records:
procedure_id
grantee
privilege = EXECUTE
grantor
REST route resolution reads the same canonical routine catalog/registry.
Do not persist a parallel system.http_functions registry.
16. Implementation requirements
Dialect
Support and retain:
SECURITY INVOKER
SECURITY DEFINER
GRANT EXECUTE ON PROCEDURE ... TO ...
REVOKE EXECUTE ON PROCEDURE ... FROM ...
Contract compiler
Include in canonical hash/diff:
routine owner
security mode
routine ACLs
HTTP adapter
Implement:
POST /v1/functions/{schema}/{procedure}
and map request JSON through the existing signature-aware typed binder.
Execution context
Store immutable root actor/request metadata and frame-local effective principal/security mode.
Runtime host API
Expose stable ctx.http request metadata and root-only response controls.
Authorization
Order for a direct HTTP/SQL procedure call:
1. authenticate root caller
2. resolve procedure
3. check caller has EXECUTE
4. create root actor/session identity
5. select effective principal from routine security mode
6. invoke procedure
7. all DB operations use normal authorization + RLS for effective principal
8. validate return
9. commit
10. format edge response
17. Required tests
Cover:
POST route resolves schema/procedure
REST scalar arguments
REST composite arguments
REST nested arguments
REST return JSON
HTTP header read
query/cookie read
custom response status
custom response header
custom content type
nested procedure reads inherited request
nested procedure cannot mutate root response
no ctx.http for PGWire/trigger/scheduler
PUBLIC denied by default
GRANT EXECUTE permits call
REVOKE EXECUTE denies call
SECURITY INVOKER obeys caller table grants
SECURITY INVOKER obeys caller RLS
SECURITY DEFINER uses owner privileges
SECURITY DEFINER preserves actor
nested invoker -> definer -> invoker principal transitions
frame principal restored after nested return
transaction rollback restores no partial writes
request credentials never auto-log
18. Final design rule
KalamDB procedures are the backend API contract.
POST /v1/functions/{schema}/{procedure}is only a transport adapter over the same procedure runtime.EXECUTEcontrols who may enter a procedure,SECURITY INVOKER/DEFINERcontrols whose privileges execute it, and the existing table/column/RLS engine controls what that effective principal may access.