Build with Lovable.
Add privacy in five steps.
~10 minutes. One step at a time. The last step verifies your integration end-to-end.
This pattern works with any browser-based AI coding platform — Lovable, Vercel v0, Base44, Bolt, and others. The only platform-specific step is Step 3 — where you paste your appKey into your platform's secrets UI.
Already have a Lovable app? The integration is additive — your existing app, auth, and UI stay intact. Skip to Step 04 and add the Edge Function + frontend helper. Krava just provides the encrypted chat backend.
- 01
01. Create a Lovable account
Free. ~30 seconds. Already have a project? Skip to Step 04.
Head to lovable.dev and sign up. You'll get a project canvas where you can prompt the AI to build apps.
Open lovable.dev → - 02
02. Paste the Krava starter prompt
Tells Lovable to scaffold a chat app with passkey auth + encrypted memory.
Build a [YOUR APP NAME] chat app with Krava AI. ──── INTEGRATION RULES (do not deviate) ──── ★ INSTALL THE SDK FIRST: npm install @kravalabs/api-client This package exists on npm. Do not hand-roll API calls — import from it. ★ AUTH: Supabase passkey (WebAuthn) ONLY - NO email/password. NO magic link. NO OAuth. - Use Supabase's built-in passkey/WebAuthn provider for sign-up and sign-in. - The sign-in UI is a single "Sign in with passkey" button — no email field. Server-side (Supabase Edge Function at supabase/functions/krava-session/index.ts): - Import createKravaPlatformClient from @kravalabs/api-client - Verify the Supabase JWT via supabase.auth.getUser(), then call platform.users.getOrCreate(user.id) - Return { userToken } as JSON - Add CORS headers (see snippet below) - KRAVA_APP_KEY is server-only — never import it on the frontend Client-side (React): - After passkey sign-in, POST to /functions/v1/krava-session with Authorization: Bearer <supabaseAccessToken> - Store the returned userToken in React state - For chat: raw fetch SSE to https://krava.io/api/platform/chat Authorization: Bearer <userToken> ← the token your Edge Function returned body: { message, chatId?, system? } - SSE stream: first event { chatId } — save it for the next turn subsequent events { text } — append tokens to the last message bubble [DONE] — stream complete Chat UI rules (prevents the first-message bug): 1. On Send: optimistically append { role: "user", text } and { role: "assistant", text: "" } 2. Fill the empty assistant bubble token-by-token as { text } events arrive 3. Do NOT re-fetch the chat from the database after creating it — you already have the data 4. chatId arrives in the FIRST SSE event — capture it with a useRef AI persona (inject as system param): "[YOUR PERSONA — e.g. You are a concise ADHD coach. Be warm and practical.]" Ask me only about visual style — the integration above is fully specified.Build a [YOUR APP NAME] chat app with Krava AI. ──── INTEGRATION RULES (do not deviate) ──── ★ INSTALL THE SDK FIRST: npm install @kravalabs/api-client This package exists on npm. Do not hand-roll API calls — import from it. ★ AUTH: Supabase passkey (WebAuthn) ONLY - NO email/password. NO magic link. NO OAuth. - Use Supabase's built-in passkey/WebAuthn provider for sign-up and sign-in. - The sign-in UI is a single "Sign in with passkey" button — no email field. Server-side (Supabase Edge Function at supabase/functions/krava-session/index.ts): - Import createKravaPlatformClient from @kravalabs/api-client - Verify the Supabase JWT via supabase.auth.getUser(), then call platform.users.getOrCreate(user.id) - Return { userToken } as JSON - Add CORS headers (see snippet below) - KRAVA_APP_KEY is server-only — never import it on the frontend Client-side (React): - After passkey sign-in, POST to /functions/v1/krava-session with Authorization: Bearer <supabaseAccessToken> - Store the returned userToken in React state - For chat: raw fetch SSE to https://krava.io/api/platform/chat Authorization: Bearer <userToken> ← the token your Edge Function returned body: { message, chatId?, system? } - SSE stream: first event { chatId } — save it for the next turn subsequent events { text } — append tokens to the last message bubble [DONE] — stream complete Chat UI rules (prevents the first-message bug): 1. On Send: optimistically append { role: "user", text } and { role: "assistant", text: "" } 2. Fill the empty assistant bubble token-by-token as { text } events arrive 3. Do NOT re-fetch the chat from the database after creating it — you already have the data 4. chatId arrives in the FIRST SSE event — capture it with a useRef AI persona (inject as system param): "[YOUR PERSONA — e.g. You are a concise ADHD coach. Be warm and practical.]" Ask me only about visual style — the integration above is fully specified.Paste this prompt, then add a few sentences describing the app you want to build. The Krava part handles privacy; your part handles the product.
Tip: if Lovable hits its time limit mid-build, just reply
continueand it will pick up where it left off. - 03
03. Get your Krava appKey
Your server-side API key. Copy it from the platform and paste it into Lovable's environment variables.
Get your API keyGenerate your
KRAVA_APP_KEYRegister your app on the Krava Platform, copy the App Key from the amber banner, then paste it directly into Lovable.
- 01Open /platform
Opens in a new tab. Sign in with a passkey if it's your first time.
First time? You'll see an “Identity Created” screen with a backup token — save it to your password manager immediately. It's your passkey recovery key and won't be shown again.
- 02Click
+ New app, give it a name, then click Create. An amber banner will appear — click Copy App Key.⚠ Shown once only. Save this to a password manager, encrypted note, or print and store offline. If you lose it, register a new app.
- 03
In Lovable, add it as an environment variable:
Name: KRAVA_APP_KEY
Value: paste your App Key hereGo to Project Settings → Environment Variables in Lovable, then click Add Variable.
- 01
- 04
04. Confirm the integration code
Lovable generated these files from your Step 2 prompt — confirm they exist, or paste them if anything is missing.
Lovable already generated these files. Check that
supabase/functions/krava-session/index.tsandsrc/lib/krava.tsexist in your project. If either is missing, paste the code below and tell Lovable to add it.Edge Function (Supabase) — runs server-side, keeps your appKey secret:
// supabase/functions/krava-session/index.ts import { createClient } from "https://esm.sh/@supabase/supabase-js@2"; import { createKravaPlatformClient } from "https://esm.sh/@kravalabs/api-client@0.2.0"; const cors = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "authorization, content-type", }; const platform = createKravaPlatformClient({ appKey: Deno.env.get("KRAVA_APP_KEY")!, baseUrl: "https://krava.io", }); Deno.serve(async (req) => { if (req.method === "OPTIONS") { return new Response(null, { status: 204, headers: cors }); } const supabase = createClient( Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_ANON_KEY")!, { global: { headers: { Authorization: req.headers.get("Authorization") ?? "" } } } ); const { data: { user }, error } = await supabase.auth.getUser(); if (error || !user) { return new Response("Unauthorized", { status: 401, headers: cors }); } const { userToken } = await platform.users.getOrCreate(user.id); return Response.json({ userToken }, { headers: cors }); }); // After editing: supabase functions deploy krava-session --no-verify-jwt// supabase/functions/krava-session/index.ts import { createClient } from "https://esm.sh/@supabase/supabase-js@2"; import { createKravaPlatformClient } from "https://esm.sh/@kravalabs/api-client@0.2.0"; const cors = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "authorization, content-type", }; const platform = createKravaPlatformClient({ appKey: Deno.env.get("KRAVA_APP_KEY")!, baseUrl: "https://krava.io", }); Deno.serve(async (req) => { if (req.method === "OPTIONS") { return new Response(null, { status: 204, headers: cors }); } const supabase = createClient( Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_ANON_KEY")!, { global: { headers: { Authorization: req.headers.get("Authorization") ?? "" } } } ); const { data: { user }, error } = await supabase.auth.getUser(); if (error || !user) { return new Response("Unauthorized", { status: 401, headers: cors }); } const { userToken } = await platform.users.getOrCreate(user.id); return Response.json({ userToken }, { headers: cors }); }); // After editing: supabase functions deploy krava-session --no-verify-jwtFrontend helper (React) — streams chat from the browser:
// src/lib/krava.ts // Demo cache. In production, scope by userId and respect token TTL. let cachedToken: string | null = null; export async function getKravaToken(userId: string): Promise<string> { if (cachedToken) return cachedToken; const res = await fetch("/functions/v1/krava-session", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ externalUserId: userId }), }); if (!res.ok) throw new Error(`Token fetch failed: ${res.status}`); const { userToken } = await res.json(); cachedToken = userToken; return userToken; } export async function* streamKravaChat( message: string, userId: string ): AsyncGenerator<string> { const userToken = await getKravaToken(userId); const res = await fetch("https://krava.io/api/platform/chat", { method: "POST", headers: { "Content-Type": "application/json", // userToken is the per-user bearer token from POST /api/platform/users. // Safe in the browser — it is the user's own encryption key. Authorization: `Bearer ${userToken}`, }, body: JSON.stringify({ message }), }); if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error(body.error ?? `Chat failed (${res.status})`); } if (!res.body) throw new Error("Streaming not supported in this environment"); const reader = res.body.getReader(); const decoder = new TextDecoder(); let buf = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buf += decoder.decode(value, { stream: true }); const lines = buf.split("\n"); buf = lines.pop() ?? ""; for (const line of lines) { if (!line.startsWith("data: ")) continue; const data = line.slice(6).trim(); if (data === "[DONE]") return; try { const parsed = JSON.parse(data); if (parsed.text) yield parsed.text; } catch { // skip malformed SSE keep-alive frame } } } }// src/lib/krava.ts // Demo cache. In production, scope by userId and respect token TTL. let cachedToken: string | null = null; export async function getKravaToken(userId: string): Promise<string> { if (cachedToken) return cachedToken; const res = await fetch("/functions/v1/krava-session", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ externalUserId: userId }), }); if (!res.ok) throw new Error(`Token fetch failed: ${res.status}`); const { userToken } = await res.json(); cachedToken = userToken; return userToken; } export async function* streamKravaChat( message: string, userId: string ): AsyncGenerator<string> { const userToken = await getKravaToken(userId); const res = await fetch("https://krava.io/api/platform/chat", { method: "POST", headers: { "Content-Type": "application/json", // userToken is the per-user bearer token from POST /api/platform/users. // Safe in the browser — it is the user's own encryption key. Authorization: `Bearer ${userToken}`, }, body: JSON.stringify({ message }), }); if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error(body.error ?? `Chat failed (${res.status})`); } if (!res.body) throw new Error("Streaming not supported in this environment"); const reader = res.body.getReader(); const decoder = new TextDecoder(); let buf = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buf += decoder.decode(value, { stream: true }); const lines = buf.split("\n"); buf = lines.pop() ?? ""; for (const line of lines) { if (!line.startsWith("data: ")) continue; const data = line.slice(6).trim(); if (data === "[DONE]") return; try { const parsed = JSON.parse(data); if (parsed.text) yield parsed.text; } catch { // skip malformed SSE keep-alive frame } } } } - 05
05. Deploy and verify
Publish your app, open it in a new tab, send a message.
⚠ Test in a new browser tab, not the Lovable preview pane. WebAuthn (passkey creation) is blocked inside Lovable's preview iframe.
Click Publish, then open the live URL in a new tab. Sign in with a passkey, send a message, and confirm a streamed response appears. That's it — your integration is working.
Want a full 7-probe diagnostic? In Lovable's chat, type: “Run this in the terminal:
KRAVA_APP_KEY=[your appKey] npx @kravalabs/api-client doctor” Lovable has terminal access and will show you the output.You shipped. Your Lovable app has passkey identity, encrypted memory, and zero-retention inference. Tweet at us when it's live.