Khazna (خزنة, "vault") is a private, offline pocket vault for links, notes and checklists — one input that works out which of the three you pasted, a capture flow measured in about a second, app lock behind your fingerprint, and no server, no account and no sync behind any of it. Built with Expo SDK 57, React Native 0.86 and TypeScript, Arabic-first with a full right-to-left layout.
Khazna (خزنة) is Arabic for vault — the small kind, the one in the room rather than the bank. Somewhere to put the link you found at midnight and the four things to pack, that is quick to reach and yours alone.
Download#
v0.2.0 is on Google Play's internal testing track. That track has no public listing, so there is no store link here yet. Android-first, Arabic and English, light and dark, free, no ads.
The signed bundle builds locally through pnpm release:aab, which bumps the version code, derives the version name from the Conventional Commits since the last tag, prebuilds clean so the native project matches app.json, forces all four ABIs, and then verifies the signer and the merged manifest's permissions before it says where the file is. Only the upload goes through EAS; appVersionSource: "local" keeps app.json the one place a version code lives, so nothing can mint a second number behind it.
Once a build is live, the app asks Play whether a newer one exists — and if there is, Play draws its own blocking update flow. That matters more here than in an app with a server behind it: nothing about a vault can be fixed from the outside, so a fix only reaches someone who is actually running the new binary.
Screens#
Fourteen screens — every surface the app has, in dark, in light, and in Arabic. Each one is a real capture off a device running the release build, seated on Khazna's own ground with the status bar and the gesture pill cropped away. Nothing is a mockup, and nothing here is somebody else's clock and battery sitting inside a picture of ours.














Why Khazna#
The product claim is one number: opening the app and getting a paste into the vault stays near a second. Every feature is measured against it, and most of the ones that would have added a step were cut for exactly that reason.
Two decisions buy the number:
- Capture is a sheet, not a screen. The floating button, a share from the browser, the launcher long-press and the empty state all open the same sheet over whatever you were looking at. An earlier full-screen composer asked what kind of thing you were saving before it let you type — that question, and the screen transition in front of it, were the whole reason capture felt slow.
- One input takes anything. Paste a URL, a paragraph or a list of lines, and the app works out which it is: an explicit checkbox beats everything, a leading URL takes the rest of the line as its annotation, and anything else is a note. The guess is a pill you can tap to change, and converting an item folds its content across kinds without losing any of it.
Storage is one persisted store over MMKV, read synchronously at boot, so the feed is on the first frame rather than after a spinner.
Features#
Capture
- One sheet, opened from the FAB, a share from another app, a launcher long-press, or the empty state
- Detection into Link / Note / List as you type, always overridable, and never destructive to convert
- A paste banner that offers the clipboard rather than reading it behind your back
- Android share doors — a share from any app, a Direct Share shortcut,
PROCESS_TEXTon selected text, two launcher long-press actions, a Quick Settings tile and a home-screen widget showing the last three items - The shortcuts are published by the app's own local module rather than a library, because R8 defeats the reflection a
Record-shaped native argument depends on and the failure mode is silent: a swallowed promise, and an app that simply stops appearing in the share sheet - A shared link that is nothing but a URL saves with no sheet at all, with an undo
- Duplicate detection: it says when you are about to save something already in the vault
- Tags at capture time, plus a domain table that tags common links by itself
Links
- Page titles and a few sentences of the page fetched on demand, cached, cancellable, and only ever for a link you saved — so the item stays readable after the page is gone
- Tracking parameters (
utm_*,fbclid,gclidand the rest) stripped as the link is normalised - Every deliberate departure goes through one seam, so opening a saved link does not come back to a lock prompt
Notes and checklists
- Plain text, committed as you type — no save button, no formatting menus
- Paste a list and every line becomes an entry; tick, drag by the grip, clear the finished ones
- A progress ring that fills as a list closes out
Finding things again
- Search as you type across titles, bodies, URLs and checklist entries
- Filter by kind and by tag, pin to the top, archive, read / unread
- Sort by saved date, title or kind — or pick "My order" and drag the vault into the order you want
- Swipe a row to pin or delete, with undo; multi-select to act on many at once
- Recently deleted holds a deleted item for 30 days before it is really gone, so undo is not the only way back
- Reminders scheduled locally, with a launch-time reconcile so nothing reads the clock during render
Privacy
- App lock behind fingerprint, face or device PIN, with a grace window and an escape hatch when biometrics become unavailable
- Screen privacy (
FLAG_SECURE) — out of screenshots and out of the app switcher, only when you turn it on - Backup to a file you pick, restore on a new device, erase everything from one screen
- Anonymous usage analytics, opt-out, that the type system will not let carry anything you typed
Craft
- Arabic-first with native RTL — the tree comes down and reloads around the direction flip, because a mounted navigator cannot absorb one
- Dark and light drawn separately, from one palette held in two syntaxes, with a test that fails the build when they drift
- Tajawal for Arabic and Rubik for Latin, and no named line-heights anywhere: Android trades the font's own padding band for one and clips Arabic marks
- Western digits in both languages, except inside Arabic running text, where two named helpers opt into Arabic-Indic
- A deep-import icon barrel, enforced by a test, because the lucide index evaluates ~1,600 icons otherwise
- 24 test suites, 216 tests, and a screenshot harness that sweeps every surface — including the three reached only by touching the screen — so a layout defect is found rather than reported
Tech stack#
| Layer | Choice |
|---|---|
| Framework | Expo SDK 57 (managed) + Expo Router (file-based routing) |
| Runtime | React Native 0.86 / React 19.2, React Compiler enabled |
| Language | TypeScript, strict mode |
| UI | NativeWind 4 (Tailwind class names on native) |
| Animation | react-native-reanimated 4 + react-native-gesture-handler |
| Vector | react-native-svg (the mark, the progress rings) |
| State | Zustand, persisted to react-native-mmkv v4 (Nitro module) |
| Notifications | expo-notifications — local reminders only, no push server, no token |
| Security | expo-local-authentication (app lock) · expo-screen-capture (FLAG_SECURE) |
| Sharing | expo-share-intent · a local Expo module for the Direct Share target, the launcher shortcuts, the Quick Settings tile and the widget |
| Files | expo-document-picker + expo-file-system (backup and restore) |
| Updates | expo-in-app-updates — Play's own blocking update flow. No OTA channel: shipping one is a promise to keep publishing to it |
| Analytics | PostHog, opt-out, restricted by an allowlist the tests enforce |
| Fonts | Rubik for Latin, Tajawal for Arabic — one family per language, because the Latin build has no Arabic glyphs at all and would fall back silently |
| Icons | lucide-react-native, deep-imported through a generated barrel |
Network: two calls, both from the device straight to a third party, both switchable off — a GET for a saved link's <title> and an excerpt, and anonymous analytics. Two connections no feature initiates are disclosed rather than hidden behind that number: the expo-updates check against Expo's servers, and the version question Play Core asks the Play Store. There is no backend: no server, no account, no sync, no API key.
Non-negotiables#
These are load-bearing — every change must respect them:
- Vault content never leaves the device. Not titles, bodies, URLs, checklist labels, tag names or search queries. Not truncated, not hashed, not "just the domain". The analytics payload types can only express counts, enums and booleans, and a test fails the build if a free string ever appears in one.
- No screen may claim the app tracks nothing. Analytics ship, so the README, the onboarding cards and the settings copy all say the true thing.
- Capture stays near a second. A feature that adds a step to that path has to justify itself against the number.
- Every screen scrolls. Content that fits still scrolls when the font scale is large or the keyboard is up.
- Reuse is not negotiable. A redesign changes how a shared primitive looks; it does not license a second hand-rolled copy of it.
- Kind colour is semantic. Slate blue is a link, sand is a note, moss is a list, and none of the three appears on anything that is not that kind.

