Architecture
This page describes how Kirak is structured internally. It is intended for contributors and developers who want to understand how the runtime works, not just how to use it. If you are building an application, start with Getting Started.
What Kirak Does
Section titled “What Kirak Does”Kirak reads your model JSON files at startup and uses them to drive every decision: which database tables to create, which API routes to generate, which roles can access which operations, and which fields are searchable. You describe your data and access policy; Kirak handles the rest.
The same JSON that defines your schema also defines your security policy. Changing the model file changes the API, the migration, and the access rules – all from one place.
Request Lifecycle
Section titled “Request Lifecycle”Every request goes through the same pipeline regardless of which model or operation it targets:
HTTP Request | vBody-size-limit middleware Rejects requests over 10 MB (Content-Length) with a 413 before any parsing. | vSecurityHeadersMiddleware Sets X-Content-Type-Options, X-Frame-Options, X-XSS-Protection, Referrer-Policy. | vRequestContextMiddleware Assigns a UUID request_id; injects request_id and user_id into log context. | vMonitoringMiddleware No-op unless the monitoring module is enabled; otherwise records request timing. | vFastAPI Router Selects the route handler for the model and operation. | vdispatch() pipeline 1. Resolve current_user from JWT (or bypass in script mode) 2. process_request(): coerce query-string types to Python types 3. run_hooks(before_{operation}, payload) 4. AccessPolicyEngine.check_and_get_rls(): verify role and apply RLS condition 5. Execute operation: validate fields -> build SQL -> execute -> serialize 6. run_hooks(after_{operation}, result) | vJSONResponse -> ClientHooks and access checks are separate steps. Access control is always enforced before any operation executes, regardless of what hooks do.
Core Concepts
Section titled “Core Concepts”The Kirak Instance
Section titled “The Kirak Instance”A single Kirak object coordinates the whole runtime: it loads models, manages the database connection pool, initialises modules, and exposes all CRUD operations as methods. Application code accesses it via request.app.get_kirak() or app.state.kirak.
Optional modules (notifications, payments, ai, storage, scheduler, monitoring) are lazy-loaded: they are imported and initialised only on first access. Unused modules cost nothing at startup.
Models
Section titled “Models”Every model in models.json defines two things:
| Key | Purpose |
|---|---|
schema |
Data structure: field types, constraints, defaults, search behaviour |
access |
Security policy: who can call each operation and under what row-level conditions |
Schema and access are intentionally separate so you can change your data structure without touching your security rules, and vice versa.
Four fields are managed automatically by Kirak – never put them in your schema block:
| Field | When set |
|---|---|
id |
INSERT |
created_at |
INSERT |
updated_at |
INSERT and UPDATE |
deleted_at |
Soft-delete; cleared on restore |
Operations
Section titled “Operations”Kirak provides 11 CRUD operations per model:
| Operation | HTTP | Description |
|---|---|---|
fetch |
GET | Query records with filters, pagination, ordering |
search |
GET | Full-text search across searchable fields |
count |
GET | Count matching records |
exists |
GET | Boolean existence check |
create |
POST | Insert one or many records |
update |
PUT | Update single or bulk records |
upsert |
PUT | Insert or update on conflict |
delete |
DELETE | Soft delete (sets deleted_at) |
destroy |
DELETE | Hard delete (permanent) |
restore |
PATCH | Un-soft-delete a record |
graphql |
POST | GraphQL query interface |
Each operation runs through the full hook + permission + validation + SQL pipeline.
Hooks are registered in on_kirak_ready and run before or after any operation or auth event. They receive the payload (before hooks) or result (after hooks) and must return it.
Hooks can be sync (def) or async (async def), and the two behave differently. A sync hook that raises stops the hook chain immediately and rejects the operation – use this for validation or policy checks that must block the request. An async hook that raises or times out is logged and skipped; the chain continues with the previous payload – use this for side effects that should not block the request. Business logic belongs in hooks, not in runtime code. Hooks run serially in registration order.
Access Policy Engine
Section titled “Access Policy Engine”The AccessPolicyEngine runs on every request before any SQL executes. It combines RBAC, row-level security, and field-level security into a single stateless decision:
- Does the model have an
accessblock? If not: deny (403) – there is no open mode. - Is the current user’s role listed for this operation? If not: 403.
- Does the matching rule have a
condition? If yes: append parameterised WHERE clause. - Does the matching rule have
access.fields? If yes: restrict which fields the role may read or write – fields absent fromaccess.fieldsare unrestricted. See Access Control.
All condition values are parameterised – never string-interpolated into SQL.
Response Envelope
Section titled “Response Envelope”Every endpoint returns the same shape:
{ "statusCode": 200, "status": "success", "message": "Records fetched successfully", "data": [...], "pagination": { "limit": 20, "page": 1, "total": 156, "total_pages": 8, "has_more": true }}The pagination key is present only when the caller passed page or offset; it is absent from single-record and write responses.
Errors use the same envelope with "status": "error" and a sanitised message. Database errors never expose internal SQL to clients.
Module System
Section titled “Module System”All modules – Auth, CrudEngine, Notifications, Payments, Storage, Scheduler, AI, Monitoring, Vector, MCP – extend BaseModule. BaseModule provides:
- Hook registry (
defaultdictkeyed by(model_name, hook_type)) dispatch()pipeline: auth -> before hooks -> operation -> after hooksrun_hooks()with asyncio timeout and error isolation- Rotating log file with request correlation IDs
execute_query()for raw SQL access (skips access rules and hooks; prefer the CRUD operations and GraphQL)HookBuilderfor fluent hook registration
Auth and CrudEngine are always loaded. Optional modules are lazy-loaded on first access using double-checked locking to prevent duplicate initialisation under concurrent requests.
Asset Resolution
Section titled “Asset Resolution”Modules that ship bundled assets (email templates, for example) resolve them through resolve_asset(module, *parts, assets_path=None) (kirak/core/asset_resolver.py) instead of hardcoding a path. It checks, in order:
{assets_path or "assets"}/{module}/{parts}under the project root – lets a project override a built-in asset without touching Core- The core package’s own bundled copy, as a fallback
Path traversal outside these two roots is rejected. This is how, for example, a project can override the built-in welcome email template by dropping its own assets/notifications/templates/email/welcome.py in the project root – no Core changes needed.
See Building Custom Modules for how to build and register your own module.
Application Lifecycle
Section titled “Application Lifecycle”When using create_kirak_app():
STARTUP 1. on_startup(app) -- external services (Redis, etc.) 2. Kirak(app, models_path) -- load + validate models.json, load kirak.json (+ kirak.local.json merged over it) 3. kirak.connect() -- create DB pool, init Auth + CrudEngine 3b. Resolve effective modules -- caller arg > kirak.json > empty 4. app.state.kirak = kirak -- make instance available to routes 5. on_kirak_ready(kirak) -- register hooks, wire custom logic 6. kirak.mount_routers() -- attach all FastAPI routes 7. scheduler.start() -- if scheduler in modules
---- APPLICATION RUNS ----
SHUTDOWN 8. scheduler.stop() -- drain job queue 9. close_redis_client() -- if Redis was used 10. kirak.disconnect() -- close DB pool 11. on_shutdown(app) -- external service cleanupHooks registered in step 5 are guaranteed to be in place before the first request.
Exception Hierarchy
Section titled “Exception Hierarchy”KirakException (500) -- base+-- ValidationError (400) -- invalid input+-- AuthenticationError (401) -- missing or invalid JWT+-- PermissionDenied (403) -- role not allowed+-- NotFoundError (404) -- resource does not exist+-- DatabaseError (500) -- DB error; message sanitised before client response+-- ConfigurationError (500) -- invalid models.json or runtime configAll are importable from the top-level package: from kirak import ValidationError, PermissionDenied.
Technology Stack
Section titled “Technology Stack”| Concern | Library |
|---|---|
| Web framework | FastAPI |
| Configuration | Pydantic Settings |
| MySQL driver | asyncmy |
| PostgreSQL driver | asyncpg |
| JWT | PyJWT |
| Password hashing | bcrypt |
| Social OAuth | social-auth-core |
| TOTP / MFA | pyotp |
| Image processing | Pillow |
| GraphQL | graphql-core |
| MCP transport | mcp (optional extra) |
| Environment vars | python-dotenv |
What to Read Next
Section titled “What to Read Next”| Topic | Document |
|---|---|
| Build a custom module | Building Custom Modules |
| How to contribute | How to Contribute |
| All application APIs | Getting Started |