Redis Integration
Redis is an optional but recommended addition for production deployments. When KIRAK_REDIS_URL is set, three database-backed auth utilities are transparently upgraded to Redis equivalents.
Install:
pip install "kirak[redis]"Configuration
Section titled “Configuration”KIRAK_REDIS_URL=redis://localhost:6379URL formats:
# No authenticationKIRAK_REDIS_URL=redis://localhost:6379
# With passwordKIRAK_REDIS_URL=redis://:your-password@localhost:6379
# TLS (managed Redis -- Upstash, Redis Cloud, ElastiCache)KIRAK_REDIS_URL=rediss://your-endpoint:6380
# With user + password (Redis 6+ ACLs)KIRAK_REDIS_URL=redis://username:password@localhost:6379For a scheduler Redis backend that uses TLS on a redis:// URL (unusual but some providers), force TLS
with the provider’s redis_ssl setting in kirak.json:
{ "scheduler": { "providers": { "redis": { "type": "redis", "redis_ssl": true } } } }What Changes with Redis
Section titled “What Changes with Redis”| Feature | Without Redis | With Redis |
|---|---|---|
| Rate limiting (CRUD and auth endpoints alike) | DB fixed-window (kirak_rate_limits table) |
Redis sliding-window sorted set – more accurate, auto-expires |
| Token blacklist | DB row (auth_token_blacklist table) |
Redis key with per-token TTL – automatically cleaned up |
| OTP storage | DB row (auth_otp table) |
Redis key with OTP TTL – automatically cleaned up |
| API key lookup | DB query on every request | Cached in Redis for 60 seconds |
CRUD query caching (fetch/search/count/exists, REST and GraphQL field queries) |
No caching – every read hits the DB | TTL-bound query cache, per-model configurable |
The first four switch transparently just by setting KIRAK_REDIS_URL – no code changes, no migration needed. The kirak_rate_limits, auth_token_blacklist, and auth_otp tables remain in the schema but are unused while Redis is active. The 60-second API key cache means a revoked key can still be accepted for up to 60 seconds after revocation – see API Keys.
Query caching is different: it needs Redis configured and an explicit opt-in (kirak.json’s cache.enabled, or a per-model "cache" block) – unlike the other four, it doesn’t turn on just because KIRAK_REDIS_URL is set. See Query Caching below.
The Redis client also pings the server every 30 seconds and auto-reconnects on failure – it’s more than pure lazy connection setup.
Rate Limiting (Sliding Window)
Section titled “Rate Limiting (Sliding Window)”With Redis, both the hardcoded auth endpoint limits and per-model CRUD limits use a sliding window algorithm instead of a fixed window, enforced atomically via a Lua script:
- Fixed window (DB): counts requests in fixed time buckets. A burst at the bucket boundary (e.g. 5 at 11:59 + 5 at 12:00) passes as two separate windows.
- Sliding window (Redis): counts requests in a rolling window ending at “now”. The burst above is seen as 10 requests in the window and is correctly blocked.
Rate limit keys in Redis follow: kirak:rl:{key}, e.g. kirak:rl:login:ip:192.168.1.1 for the hardcoded auth limits, or kirak:rl:crud:posts:fetch:ip:192.168.1.1 for a per-model CRUD limit. See Rate Limiting for the full key format per scope, global defaults, per-operation config, and the admin API for inspecting/resetting individual counters.
Query Caching (Generational Invalidation)
Section titled “Query Caching (Generational Invalidation)”CRUD reads (fetch, search, count, exists – REST routes, script mode, and GraphQL field queries all share the same underlying functions) can be cached in Redis, keyed so two users entitled to see different rows never share a cache entry, and automatically invalidated whenever the model they read from is mutated.
Enable it in kirak.json for a framework-wide default:
{ "cache": { "enabled": true, "default_ttl_seconds": 60 } }Or per-model in models.json, sibling to rate_limit:
"cache": { "enabled": true, "ttl_seconds": 60}A model without a "cache" block inherits the kirak.json default. "enabled": false is an explicit, always-boolean opt-out (never an empty {} sentinel).
Cache keys follow a generational scheme, not per-row invalidation:
kirak:cache:gen:{model_name} -> integer, INCR'd on invalidationkirak:cache:q:{model_name}:{generation}:{sha256(sql+params)} -> cached response, TTL-boundSince row-level security is baked directly into the SQL string before it reaches Redis, two different users querying the same model with the same filters get different cache keys automatically whenever their RLS scope differs – there’s no separate “scope by user” logic to get wrong.
A create/update/delete/upsert/destroy/restore on a model INCRs that model’s generation counter, which makes every previously-cached entry for it unreachable (old entries just expire on their own TTL rather than being actively deleted) – an O(1) operation with no SCAN/KEYS pattern-delete, which is the usual way naive Redis cache invalidation becomes an incident on a busy keyspace.
GraphQL mutations get one extra step: a mutation document can carry several root fields sharing one transaction, so a write isn’t durable until the whole document commits. Rather than invalidating too early (which could let a concurrent read cache stale data under the new generation, where nothing would catch it again until the TTL expires), GraphQL mutations queue their invalidations and flush them once, immediately after the transaction actually commits – and never flush at all if the transaction rolls back.
Without Redis, or with caching disabled: every read goes straight to the DB, exactly as before – no code changes required in an application either way.
Token Blacklist (Per-Token TTL)
Section titled “Token Blacklist (Per-Token TTL)”When logout is called, the token’s jti is added to Redis with the key kirak:bl:{jti} and a TTL equal to the token’s remaining lifetime. When the token expires, the Redis key expires automatically – no cleanup job needed.
Without Redis, blacklist entries accumulate in the auth_token_blacklist table and must be periodically purged.
OTP Storage (Auto-Expiry)
Section titled “OTP Storage (Auto-Expiry)”OTP codes are stored as kirak:otp:{phone_number} with a short TTL. When the TTL expires, the OTP is automatically invalidated. No background job or explicit deletion is required.
Fail-Open Behavior
Section titled “Fail-Open Behavior”Redis utilities use fail-open semantics – if Redis becomes unavailable, the request is allowed through – except the token blacklist, which fails closed.
Specifically:
- Rate limiting: if the Redis check fails, the request is allowed (no limit enforced).
- Token blacklist: if the Redis check fails, the token is refused with 503
AUTH_UNAVAILABLE, and a logout that cannot write the blacklist fails with the same code – a logged-out token never works. - OTP: if the Redis check fails, the OTP is considered valid (best-effort).
The trade-off: during a Redis outage rate limits are temporarily unenforced, and authenticated requests fail with 503 until Redis is back (requests without a token, and the public auth routes, are unaffected).
Scheduler Redis Backend
Section titled “Scheduler Redis Backend”Redis is also used as the Scheduler queue backend (separate from auth Redis). The provider choice is a kirak.json setting, not an environment variable:
{ "scheduler": { "default_provider": "redis", "providers": { "redis": { "type": "redis" } } }}KIRAK_SCHEDULER_REDIS_URL=redis://localhost:6379The scheduler Redis backend uses list-based queues and is independent of the auth Redis integration. Both can point to the same Redis instance or separate ones.
Checking Connection at Startup
Section titled “Checking Connection at Startup”Kirak does not currently validate the Redis connection at startup – it initializes lazily on first use. If you want to assert Redis availability at boot:
def on_kirak_ready(kirak): import os redis_url = os.getenv("KIRAK_REDIS_URL", "") if redis_url: import asyncio import redis.asyncio as aioredis async def check(): r = await aioredis.from_url(redis_url) await r.ping() await r.aclose() asyncio.get_event_loop().run_until_complete(check()) print("Redis connection verified")