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.
Prerequisites
Section titled “Prerequisites”- Python 3.10+
- MySQL 8+ or PostgreSQL 13+ – one must be reachable before you run migrations
- pip (bundled with Python)
Installation
Section titled “Installation”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:
pip install "kirak[mysql]"PostgreSQL:
pip install "kirak[postgres]"Both drivers (useful for teams that test against both):
pip install "kirak[all-databases]"Verify the install:
kirak --helpCreate a New Project
Section titled “Create a New Project”The kirak new command scaffolds a complete project directory:
kirak new my-api # MySQL (the default)kirak new my-api --database postgres # PostgreSQLcd my-apipip 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 hereConfigure Environment Variables
Section titled “Configure Environment Variables”Copy the example file and fill in your database credentials:
cp .env.example .envOpen .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 passwordDB_PASSWORD=your-password
# Auth -- required; startup fails fast if either key is missing or shorter than 32 bytesKIRAK_AUTH_JWT_SECRET_KEY=replace-this-with-a-32-byte-or-longer-secretKIRAK_AUTH_JWT_REFRESH_SECRET_KEY=another-32-byte-or-longer-secret-here
# Email verification and password reset linksKIRAK_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 Fernetprint(Fernet.generate_key().decode())Understand the Generated Model
Section titled “Understand the Generated Model”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 nameschema– your field definitions. Do not addid,created_at,updated_at, ordeleted_at– Kirak manages those automaticallysoft_delete: true– deletes setdeleted_atinstead of removing the row; deleted records are invisible to all fetch/search/count queriesaccess– per-operation role rules. Theconditionfield adds a row-level WHERE clause automatically parameterised with the current user’s claims ({user_id}resolves to the token’suser_id). Models with noaccessblock 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.
Run the Initial Migration
Section titled “Run the Initial Migration”kirak db init generates and immediately applies the initial SQL migration from your models (plus the built-in auth tables):
kirak db initOutput:
Created: migrations/20250801100000_initial.sqlSnapshot: 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:
# Add a field to models/posts.json, then:kirak db makemigrations add-status-fieldkirak db migrateStart the Server
Section titled “Start the Server”The generated main.py is minimal by design:
from dotenv import load_dotenvload_dotenv()
from kirak import create_kirak_app
app = create_kirak_app( models_path="./models/",)Start it with uvicorn:
uvicorn main:app --reloadThe server starts on http://localhost:8000. Auto-reload is active for development.
Explore the Auto-Generated API
Section titled “Explore the Auto-Generated API”API Docs
Section titled “API Docs”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.
Auth Endpoints
Section titled “Auth Endpoints”Every Kirak app ships with a full auth system at /auth:
# Register a new usercurl -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"}'
# Logincurl -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 }}CRUD Endpoints
Section titled “CRUD Endpoints”Use the access token to call your model’s endpoints:
export TOKEN="eyJhbGc..."
# Create a post -- user_id is filled in from the token by the create conditioncurl -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 paginationcurl "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 deletecurl -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.
Add Your First Hook
Section titled “Add Your First Hook”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 resultWire it into main.py via the on_kirak_ready callback:
from dotenv import load_dotenvload_dotenv()
from kirak import create_kirak_appfrom 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.
Enable Optional Modules
Section titled “Enable Optional Modules”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) |
Project Layout Convention
Section titled “Project Layout Convention”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.jsonMultiple JSON files in models/ are merged automatically at startup. A ConfigurationError is raised if two files define a model with the same name.
What to Read Next
Section titled “What to Read Next”| 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 |