Skip to content

Getting Started

This guide takes you from zero to a running Kirak API in about five minutes. By the end you will have a project with CRUD endpoints, JWT authentication, and a working database migration.


  • Python 3.10+
  • MySQL 8+ or PostgreSQL 13+ – one must be reachable before you run migrations
  • pip (bundled with Python)

The base package ships with FastAPI, authentication, GraphQL, and the CLI. A database driver is a separate optional extra because you only install the one you need, so install kirak[mysql] or kirak[postgres], not plain kirak: without a driver the app cannot connect, and kirak validate reports database_driver_not_installed.

MySQL:

Terminal window
pip install "kirak[mysql]"

PostgreSQL:

Terminal window
pip install "kirak[postgres]"

Both drivers (useful for teams that test against both):

Terminal window
pip install "kirak[all-databases]"

Verify the install:

Terminal window
kirak --help

The kirak new command scaffolds a complete project directory:

Terminal window
kirak new my-api # MySQL (the default)
kirak new my-api --database postgres # PostgreSQL
cd my-api
pip install -e .

--database sets the database block of kirak.json and the dependency in pyproject.toml: kirak[mysql] or kirak[postgres]. pip install -e . installs that dependency, so the project gets its driver even when Kirak itself was installed without one. Use a virtual environment per project.

This creates:

my-api/
+-- main.py # Application entry point
+-- kirak.json # Project manifest (modules, CORS settings)
+-- .env.example # The secrets the project needs
+-- .gitignore
+-- pyproject.toml
+-- AGENTS.md # Instructions for AI coding agents
+-- .kirak/ # JSON Schemas, for editors and agents
+-- .vscode/
| +-- settings.json # Maps the config files to the schemas
+-- models/
| +-- posts.json # Your data models, one per file
+-- hooks/
| +-- __init__.py
| +-- post_hooks.py # Hook stubs to fill in
+-- migrations/ # Auto-generated SQL files live here

Copy the example file and fill in your database credentials:

Terminal window
cp .env.example .env

Open .env and set the secrets. The database type, host, port, user and name are in the database block of the scaffolded kirak.json; only the password is a secret:

# Database password
DB_PASSWORD=your-password
# Auth -- required; startup fails fast if either key is missing or shorter than 32 bytes
KIRAK_AUTH_JWT_SECRET_KEY=replace-this-with-a-32-byte-or-longer-secret
KIRAK_AUTH_JWT_REFRESH_SECRET_KEY=another-32-byte-or-longer-secret-here
# Email verification and password reset links
KIRAK_AUTH_VERIFICATION_KEY=your-fernet-key

.env holds secrets only; every other setting goes in kirak.json, and Kirak never reads a setting from the environment. Your app’s own settings go in the custom block of kirak.json (an unknown top-level key stops startup) and are read at runtime as kirak.manifest.custom; your app’s own secrets go in .env. See Configuration.

Generate a valid Fernet key for KIRAK_AUTH_VERIFICATION_KEY:

from cryptography.fernet import Fernet
print(Fernet.generate_key().decode())

Open models/posts.json. The scaffold creates a posts model as a starting point; each file in models/ is one model, named after the file:

{
"$schema": "../.kirak/model.schema.json",
"table": "posts",
"soft_delete": true,
"schema": {
"title": { "type": "string", "required": true },
"content": { "type": "text" },
"user_id": { "type": "integer", "required": true }
},
"access": {
"fetch": [{ "role": "user", "condition": "user_id = {user_id}" }, { "role": "admin" }],
"search": [{ "role": "user", "condition": "user_id = {user_id}" }, { "role": "admin" }],
"count": [{ "role": "user", "condition": "user_id = {user_id}" }, { "role": "admin" }],
"exists": [{ "role": "user", "condition": "user_id = {user_id}" }, { "role": "admin" }],
"create": [{ "role": "user", "condition": "user_id = {user_id}" }, { "role": "admin" }],
"update": [{ "role": "user", "condition": "user_id = {user_id}" }, { "role": "admin" }],
"delete": [{ "role": "user", "condition": "user_id = {user_id}" }, { "role": "admin" }],
"destroy": [{ "role": "admin" }],
"restore": [{ "role": "admin" }]
}
}

The sample is owner-only: a user reads, searches, changes and deletes only their own posts, and on create the condition makes Kirak fill in user_id from the token, so a client cannot create a post for someone else (sending another user_id is refused). Admins can do everything; hard delete (destroy) and restore are admin-only. To make posts public, give the read operations to * – and keep the owner condition on the writes.

$schema lets your editor complete and check the file; Kirak ignores it.

Key things to know:

  • table – the database table name
  • schema – your field definitions. Do not add id, created_at, updated_at, or deleted_at – Kirak manages those automatically
  • soft_delete: true – deletes set deleted_at instead of removing the row; deleted records are invisible to all fetch/search/count queries
  • access – per-operation role rules. The condition field adds a row-level WHERE clause automatically parameterised with the current user’s claims ({user_id} resolves to the token’s user_id). Models with no access block deny every operation, so every model you expose needs one

The auth tables (users, auth_tokens, auth_social, auth_mfa, auth_otp, and supporting tables) are built into Kirak and are always included in migrations automatically – you do not define them yourself.

For a complete field type reference see Defining Models.


kirak db init generates and immediately applies the initial SQL migration from your models (plus the built-in auth tables):

Terminal window
kirak db init

Output:

Created: migrations/20250801100000_initial.sql
Snapshot: migrations/models_snapshot.json
Applying migration...
Done -- 1 applied, 0 already applied.

The generated file contains both MySQL and PostgreSQL DDL in clearly marked sections; only the section matching database.type in kirak.json is executed.

After init, use kirak db makemigrations to generate incremental migrations as your models evolve:

Terminal window
# Add a field to models/posts.json, then:
kirak db makemigrations add-status-field
kirak db migrate

The generated main.py is minimal by design:

from dotenv import load_dotenv
load_dotenv()
from kirak import create_kirak_app
app = create_kirak_app(
models_path="./models/",
)

Start it with uvicorn:

Terminal window
uvicorn main:app --reload

The server starts on http://localhost:8000. Auto-reload is active for development.


http://localhost:8000/docs describes the running API in Markdown – models, CRUD and GraphQL, modules and your own routes – written for AI agents and readable by people. http://localhost:8000/openapi.json is the OpenAPI document, with one set of CRUD paths per model, for API clients such as Postman, Insomnia or Bruno. See API Docs Endpoint.

Every Kirak app ships with a full auth system at /auth:

Terminal window
# Register a new user
curl -X POST http://localhost:8000/auth/register \
-H "Content-Type: application/json" \
-d '{"email": "alice@example.com", "password": "Secret!Pass123", "first_name": "Alice", "last_name": "Smith"}'
# Login
curl -X POST http://localhost:8000/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "alice@example.com", "password": "Secret!Pass123"}'

Login returns an access token and a refresh token:

{
"statusCode": 200,
"status": "success",
"message": "Login successful",
"data": {
"user": { "user_id": 1, "email": "alice@example.com", "role": "user" },
"accessToken": "eyJhbGc...",
"refreshToken": "eyJhbGc...",
"expiresIn": 3600
}
}

Use the access token to call your model’s endpoints:

Terminal window
export TOKEN="eyJhbGc..."
# Create a post -- user_id is filled in from the token by the create condition
curl -X POST http://localhost:8000/posts/create \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"data": {"title": "Hello Kirak", "content": "My first post"}}'
# Fetch posts with pagination
curl "http://localhost:8000/posts/fetch?limit=10&page=1" \
-H "Authorization: Bearer $TOKEN"
# Update (only your own post -- RLS enforced)
curl -X PUT http://localhost:8000/posts/update \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"where": {"id": 1}, "data": {"title": "Updated Title"}}'
# Soft delete
curl -X DELETE "http://localhost:8000/posts/delete?id=1" \
-H "Authorization: Bearer $TOKEN"

Every response uses the same envelope shape. See CRUD Operations for the complete parameter reference.


Hooks let you attach business logic to any operation without touching runtime code. Open hooks/post_hooks.py:

def register_hooks(kirak):
@kirak.on("posts").hook("after_create")
async def after_post_created(result):
# result is the full response dict from the create operation
post_id = result["data"]["id"]
print(f"Post {post_id} created -- trigger side-effects here")
return result # always return the result

Wire it into main.py via the on_kirak_ready callback:

from dotenv import load_dotenv
load_dotenv()
from kirak import create_kirak_app
from hooks.post_hooks import register_hooks
app = create_kirak_app(
models_path="./models/",
on_kirak_ready=register_hooks,
)

Restart the server, create a post, and you will see the print in the terminal. Replace the print with email sends, job enqueueing, audit logging, or anything else – the hook receives the full result dict and passes it along the chain.

Hooks can also be plain def functions: a sync hook that raises stops the operation, which is how a before_* hook rejects a request. An async hook that fails is logged and skipped.

See Hooks & Events for the complete hook reference including auth hooks and all event types.


Modules are opt-in. Add them to modules in create_kirak_app() or declare them in kirak.json:

app = create_kirak_app(
models_path="./models/",
modules=["notifications", "payments", "storage", "scheduler"],
on_kirak_ready=register_hooks,
)

Each module requires its own extra installed:

Module Install command
MFA (TOTP authenticator app) pip install "kirak[mfa]"
Notifications (AWS SES email) pip install "kirak[notifications-ses]"
Notifications (AWS SNS SMS) pip install "kirak[notifications-sns]"
Notifications (SendGrid) pip install "kirak[notifications-sendgrid]"
Notifications (Twilio SMS) pip install "kirak[notifications-twilio]"
Notifications (push, Firebase) pip install "kirak[notifications-firebase]"
Notifications (push, Apple APNs) pip install "kirak[notifications-apn]"
Notifications (SMTP, Mailgun, Postmark, SparkPost, Brevo, Resend, Vonage, Plivo, MessageBird, Africa’s Talking, Termii, FastSMS, Huawei, Expo, Slack, Discord, Telegram, webhooks) no extra install
Notifications (every provider) pip install "kirak[all-notifications]"
Payments (Stripe) pip install "kirak[payments-stripe]"
Payments (Razorpay) pip install "kirak[payments-razorpay]"
Payments (Square) pip install "kirak[payments-square]"
Payments (PayPal, Paddle, Paystack, Flutterwave, Mercado Pago, Xendit, Airwallex, Omise, Telr) no extra install
Payments (every provider) pip install "kirak[all-payments]"
Storage (local, S3, Wasabi, R2, Spaces, Cubbit, OVHcloud, Backblaze B2) pip install "kirak[storage]"
Storage (Google Cloud Storage) pip install "kirak[storage-gcs]" (includes storage)
Storage (Azure Blob Storage) pip install "kirak[storage-azure]" (includes storage)
Storage (every provider) pip install "kirak[all-storage]"
AI (OpenAI) pip install "kirak[ai-openai]"
AI (Anthropic) pip install "kirak[ai-anthropic]"
AI (Google Gemini) pip install "kirak[ai-google]"
AI (every provider) pip install "kirak[all-ai]"
AI (MCP servers as agent toolsets) pip install "kirak[ai-mcp]" (add to an AI extra)
Vector (Pinecone; OpenAI, Gemini and Ollama embeddings) no extra install
Vector (Amazon S3 Vectors) pip install "kirak[vector-s3vectors]"
Scheduler (Redis queue) pip install "kirak[scheduler-redis]"
Scheduler (RabbitMQ queue) pip install "kirak[scheduler-rabbitmq]"
Monitoring pip install "kirak[monitoring]"
Redis (rate limiting, blacklist, OTP) pip install "kirak[redis]"
MCP servers (the mcp module; Python 3.10+) pip install "kirak[mcp]"
Everything except a database driver pip install "kirak[all,postgres]" (or mysql)

As your project grows, a typical layout looks like:

my-api/
+-- main.py
+-- kirak.json
+-- .env
+-- .env.example
+-- pyproject.toml
+-- models/
| +-- customers.json # your models; built-in ones (users, auth_tokens, ...) cannot be redefined
| +-- posts.json
| +-- products.json
| +-- orders.json # one file per domain, or keep them merged
+-- hooks/
| +-- __init__.py
| +-- post_hooks.py
| +-- order_hooks.py
+-- migrations/
+-- 20250801100000_initial.sql
+-- 20250810143000_add_status.sql
+-- models_snapshot.json

Multiple JSON files in models/ are merged automatically at startup. A ConfigurationError is raised if two files define a model with the same name.


Topic Document
All field types, schema options, soft_delete, rate_limit Defining Models
Filters, pagination, ordering, bulk operations CRUD Operations
RBAC, row-level security, field-level access Access Control
All hook types and registration patterns Hooks & Events
All environment variables Configuration
Social login, OTP, MFA Authentication
Running as a standalone auth service Standalone Auth