By Thales & Claude -- CEO & AI CTO, ZeroSuite, Inc.
On March 3, 2026, the Deblo.ai web platform was already live. Ninety-six sessions deep, it had a working chat, 24 AI tools, voice calls, credit payments, and a full curriculum engine. The web was solid. But in West Africa, "web" is not enough. The students we are building for live on their phones. They browse on 4G with intermittent connections. They do not open laptops to study. If Deblo was going to reach 250 million African students, it needed a native mobile app -- and it needed one fast.
Seven days. Twenty sessions. One monorepo. This is how we built the Deblo K12 mobile app from scratch.
---
Why Native, and Why Now
The temptation was to wrap the SvelteKit web app in a WebView and call it a day. We considered it for about ten minutes. Here is why we rejected it:
First, voice calls. Deblo's AI voice feature relies on LiveKit and Ultravox for real-time WebRTC communication. WebView-based audio is unreliable on Android, particularly on the mid-range Samsung and Tecno devices that dominate the African market. Native WebRTC, through @livekit/react-native-webrtc, gives us direct access to the device's audio pipeline.
Second, push notifications. Students need reminders: "Your daily exercise is ready," "You have 3 unfinished tasks," "Your teacher assigned new homework." These require background notification handling that WebViews simply cannot provide.
Third, biometric authentication. Face ID and fingerprint unlock are table stakes for a mobile app in 2026. A WebView cannot access the Secure Enclave.
Fourth, performance. Chat with streaming responses, LaTeX rendering, and animated transitions -- all of this must feel native because it is competing for attention with TikTok and WhatsApp. A WebView will always feel like a WebView.
We chose React Native with Expo SDK 54, React 19.1, and TypeScript in strict mode. Not because it was the only option, but because it was the fastest path to a production-quality native app that shares logic with a web platform built on a different framework.
---
The Monorepo Architecture
The critical architectural decision was the monorepo. The Deblo web frontend is SvelteKit. The mobile app is React Native. They share a Python FastAPI backend, but the frontend logic -- API calls, state management, streaming, internationalization -- would need to be duplicated unless we extracted it.
We created four shared packages:
{
"name": "deblo-mobile",
"version": "1.0.0",
"private": true,
"workspaces": [
"apps/*",
"packages/*"
],
"scripts": {
"k12": "npm -w apps/k12 start",
"k12:ios": "npm -w apps/k12 run ios",
"k12:android": "npm -w apps/k12 run android",
"check": "npm -w apps/k12 run check"
}
}The four packages serve distinct purposes:
- @deblo/api -- Typed API client with base URL configuration, token injection, and all endpoint wrappers. Shared types like
UserProfile,Conversation,Message, andCreditPacklive here. - @deblo/stores -- Zustand stores for auth, chat, voice, settings, and currency. These replace the Svelte stores on the web side.
- @deblo/streaming -- The SSE streaming hook (
useStream) that handles real-time AI responses. This was the most complex shared package. - @deblo/i18n -- Internationalization with
i18nextandreact-i18next. French-first, with English as a secondary locale.
The apps/k12 directory contains the Expo app itself, with file-based routing via Expo Router. This structure mirrors SvelteKit's src/routes/ -- a deliberate parallel that reduced cognitive overhead when switching between web and mobile development.
---
State Management: Zustand with SecureStore
On the web, Deblo uses Svelte stores with localStorage persistence. On mobile, we needed something that works with React's component model and supports secure storage for sensitive data like JWT tokens.
Zustand was the obvious choice. It is small (1.1 KB gzipped), has no boilerplate, works outside of React components (critical for the API client), and supports middleware for persistence. Here is the auth store:
import { create } from 'zustand';
import * as SecureStore from 'expo-secure-store';
import type { UserProfile } from '@deblo/api';
import { setTokenGetter } from '@deblo/api';const JWT_KEY = 'deblo_k12_jwt';
export interface AuthState {
token: string | null;
user: UserProfile | null;
setToken: (token: string, user: UserProfile) => Promise
export const useAuthStore = create
return { token: null, user: null,
setToken: async (token, user) => { await SecureStore.setItemAsync(JWT_KEY, token); set({ token, user: normalizeUser(user) }); },
logout: async () => { await SecureStore.deleteItemAsync(JWT_KEY); set({ token: null, user: null }); },
restoreSession: async () => { const stored = await SecureStore.getItemAsync(JWT_KEY); if (stored) { // Validate token with backend, restore user profile } }, // ... }; }); ```
The key design decision: setTokenGetter(() => get().token) is called at store creation time. This injects a function into the @deblo/api package that the API client calls before every request to attach the Authorization header. Because Zustand stores are singletons, get().token always returns the current token without requiring React context. This pattern eliminated an entire class of "token not available" bugs that plague React Native apps using context-based auth.
SecureStore is used for the JWT because it leverages the iOS Keychain and Android EncryptedSharedPreferences. Regular AsyncStorage -- which we use for non-sensitive preferences like theme and language -- is unencrypted on both platforms.
---
The Custom Tab Bar
Stock tab bars in React Native look generic. Every education app uses the same bottom tabs with the same icons. We wanted something that felt distinct -- a floating green bar that reinforced Deblo K12's brand identity.
The K12 color scheme uses green (#22c55e) as its primary color, contrasting with the web platform's orange branding. This was a deliberate choice: green signals "learning" and "growth" in the African educational context, while orange signals "professional" on the Pro side.
/**
* Navigation par onglets K12 -- 5 tabs :
* Accueil | Exo | [+FAB -> modal outils] | Taches | Credits
*
* Barre flottante verte custom en absolute bottom.
* La tab bar native est masquee (display: 'none').
*/
const SCREEN_W = Dimensions.get('window').width;
const BAR_W = SCREEN_W * 0.92;
const BAR_H = 64;// Inside the Tabs layout:
{/ Custom floating tab bar /}
The center tab is a floating action button (FAB) that opens a modal with tool shortcuts: new chat, photo analysis, voice call, drawing board, and exercise generator. This pattern -- popularized by apps like Telegram and Instagram -- puts the most important actions at the user's thumb position.
The tab bar also hosts two side panels. A left drawer (76% screen width) slides in with an animated "push + scale" effect, revealing the main menu: profile, conversations, notifications, settings, and family management. A right panel provides chat history for quick conversation switching. Both use Animated.timing with native driver for 60fps performance.
---
SSE Streaming on React Native
This was the hardest technical challenge. On the web, Deblo uses the EventSource API for Server-Sent Events. React Native does not support EventSource natively. The Hermes JavaScript engine (React Native's default since 0.70) does support fetch with ReadableStream, but the behavior differs from browsers in subtle ways.
We built a custom useStream hook in the @deblo/streaming package:
export function useStream() {
const abortRef = useRef<AbortController | null>(null);const stream = useCallback( async (path: string, body: object, opts: StreamOptions) => { abortRef.current?.abort(); const controller = new AbortController(); abortRef.current = controller;
const token = useAuthStore.getState().token;
const res = await fetch(${BASE_URL}${path}, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: Bearer ${token} } : {}),
},
body: JSON.stringify(body),
signal: controller.signal,
});
if (!res.ok || !res.body) {
opts.onError(new Error(HTTP ${res.status}));
return;
}
const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let convId: string | undefined;
while (true) { const { done, value } = await reader.read(); if (done) break;
buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() ?? '';
const finished = processSSELines(lines, convId, opts); if (finished) break; } }, [], );
const cancel = useCallback(() => { abortRef.current?.abort(); }, []);
return { stream, cancel }; } ```
The processSSELines function parses each data: prefixed line as JSON and dispatches events based on type: content (streamed text tokens), title (conversation title updates), tool_start and tool_end (AI tool execution progress), suggestions (follow-up prompts), file (generated documents), quiz (interactive assessments), and credit_update (balance changes).
The critical detail is buffer management. SSE lines can be split across ReadableStream chunks, especially on slow mobile connections. We accumulate data in a buffer and only process complete lines (terminated by \n). The final lines.pop() retains any incomplete line for the next iteration. This is the same pattern used in browser-based SSE polyfills, but it must be implemented manually on React Native.
---
Authentication: Four Layers Deep
African mobile users do not have email-centric accounts. Many do not have Google accounts. Every single one has a phone number. Our authentication system reflects this reality:
1. Phone OTP -- The primary flow. User enters their phone number, receives a 6-digit code via WhatsApp (or SMS as fallback), and is authenticated. Same flow as the web, same backend endpoint.
2. Google Sign-In -- Optional, for users who have Google accounts. Uses @react-native-google-signin/google-signin with native module integration. Returns a Google ID token that the backend exchanges for a Deblo JWT.
3. Biometric unlock -- After initial authentication, users can enable Face ID or fingerprint unlock. The JWT is stored in SecureStore, and expo-local-authentication handles the biometric prompt. On subsequent app launches, the user sees a biometric prompt instead of the OTP flow.
4. Student PIN -- For younger students (CP through CM2, ages 6-12) who share a parent's phone. The parent creates student sub-accounts with 4-digit PINs. The student taps their avatar on the family screen and enters their PIN -- no phone number required, no OTP, no biometric. This flow required a dedicated /api/auth/pin-login endpoint.
The layered approach means a 7-year-old in Abidjan can access their AI tutor with a 4-digit PIN, while their parent's account is secured with biometric authentication. Both are backed by the same JWT system with the same 30-day expiry.
---
Voice Calls and the Expo Go Problem
Deblo's AI voice call feature -- powered by LiveKit and Ultravox -- was the most painful integration. LiveKit's React Native SDK (@livekit/react-native and @livekit/react-native-webrtc) requires native modules for WebRTC. This means it cannot run in Expo Go, the development companion app.
We had to switch to development builds using expo-dev-client. This added a compilation step: instead of scanning a QR code and running the app instantly in Expo Go, we needed to build a custom development client that includes the LiveKit native modules. On macOS, an iOS development build takes about 3 minutes. On a Linux CI server for Android, about 5 minutes.
The trade-off was worth it. Voice calls in the mobile app feel genuinely native. The student taps a phone icon, a voice_sessions record is created server-side, a LiveKit room is provisioned, and the Ultravox agent connects. Audio streams through the device's native audio pipeline with echo cancellation and noise suppression handled at the OS level. On the web, we fight with browser audio policies and autoplay restrictions. On mobile, the audio just works.
---
Drawing Board and Photo Analysis
Two features that only make sense on mobile:
The drawing board is a full-screen canvas where students can sketch math problems, draw geometric figures, or write equations by hand. We use react-native-svg for the canvas and expo-image-picker for the camera integration. A student can photograph a math problem from their textbook, and the AI analyzes it using GPT-4o Mini's vision capabilities.
The photo analysis flow is deceptively simple from the user's perspective: tap the camera icon, take a photo, and Deblo reads the problem and begins solving it. Behind the scenes, the image is uploaded to the backend via the /api/upload/image endpoint, a pre-signed URL is generated, and the image URL is injected into the chat message. The LLM receives the image as part of a multimodal message and generates a step-by-step solution.
This is the feature that most resonates with African parents. A child struggling with a homework exercise at 9 PM, after the school day is over and the teacher is unavailable, can photograph the exercise and receive a Socratic, step-by-step explanation in seconds. That is the promise of Deblo.
---
The MathBlock Challenge
LaTeX rendering on React Native was our white whale. On the web, we use KaTeX with HTML rendering. On mobile, there is no HTML engine. Every React Native LaTeX library we evaluated had issues:
react-native-katex-- WebView-based, slow, flickers on re-render.react-native-math-view-- Uses MathJax in a WebView, same problems.react-native-mathjax-svg-- Renders to SVG, better performance, but inconsistent rendering with complex expressions.
We settled on a hybrid approach: simple inline expressions ($x^2 + 3x = 0$) are rendered using react-native-mathjax-svg for native-feeling performance. Complex block expressions (multi-line derivations, matrices, equation systems) use a WebView-based renderer with pre-compiled KaTeX CSS. The MathBlock component detects expression complexity and routes to the appropriate renderer.
The result is not perfect. Complex LaTeX occasionally takes 200-300ms to render on mid-range devices. But it is functional, and functional beats perfect when you have seven days.
---
Push Notifications
We started with Firebase Cloud Messaging (FCM) because it is the default for Android notifications. Then we discovered Expo's Push API and switched entirely.
Expo Push handles token management, message delivery, and receipt tracking across both iOS (APNs) and Android (FCM) through a single API. When a user logs in, their Expo push token is sent to the backend and stored on the user record. The backend sends notifications through Expo's push service, which routes them to the appropriate platform.
Notification types include: daily exercise reminders (configurable time), task due date alerts, credit balance warnings (below 5 credits), new conversation responses (when the app is backgrounded during a long generation), and family activity updates (when a child completes an exercise, the parent is notified).
---
Seven Days in Numbers
The final tally for the mobile sprint:
- 20 development sessions across 7 calendar days (March 3-9, 2026)
- 4 shared packages (
@deblo/api,@deblo/stores,@deblo/streaming,@deblo/i18n) - 20+ screens (home, chat, voice call, drawing board, profile, settings, family, credits, transactions, notifications, exercises, projects, help, about, and more)
- 5 authentication methods (phone OTP, Google, biometric, student PIN, session restore)
- 1 monorepo with workspaces linking apps and packages
- React Native 0.81.5 + Expo SDK 54 + React 19.1 + TypeScript strict mode
The app was not shipped to the App Store in those seven days. Polish, testing, and store review take longer. But the core functionality -- every feature available on the web, reimplemented natively -- was complete.
---
What We Learned
Building a React Native app in parallel with a SvelteKit web app taught us three things:
First, shared packages are not optional for multi-platform products. Without @deblo/api and @deblo/streaming, we would have duplicated thousands of lines of API client and SSE parsing code. The monorepo paid for its complexity in the first two days.
Second, Expo has matured dramatically. In 2023, Expo was still a "toy" framework that serious React Native developers avoided. In 2026, with SDK 54, development builds, EAS, and the Expo Router, it is the default starting point. The file-based routing alone saved us a full day of navigation setup.
Third, mobile-first design reveals web-first assumptions. Our web chat had a 700px max-width container. On a 375px phone screen, that assumption broke every layout. Our credit display used decimal formatting. On mobile, every pixel matters, and "1,234.56" takes too much horizontal space. Every screen we built for mobile made us go back and improve the web.
Seven days is not enough to build a great mobile app. But it is enough to build a working one, if you have a solid backend and the discipline to share code across platforms.
---
This is Part 9 of a 12-part series on building Deblo.ai.
1. AI Tutoring for 250 Million African Students 2. 100 Sessions Later: The Architecture of an AI Education Platform 3. The Agentic Loop: 24 AI Tools in a Single Chat 4. System Prompts That Teach: Anti-Cheating, Socratic Method, and Grade-Level Adaptation 5. WhatsApp OTP and the African Authentication Problem 6. Credits, FCFA, and 6 African Payment Gateways 7. SSE Streaming: Real-Time AI Responses in SvelteKit 8. Voice Calls With AI: Ultravox, LiveKit, and WebRTC 9. Building a React Native K12 App in 7 Days (you are here) 10. 101 AI Advisors: Professional Intelligence for Africa 11. Background Jobs: When AI Takes 30 Minutes to Think 12. From Abidjan to 250 Million: The Deblo.ai Story