Library / SDK
SchroederNathan/clarity avatar
SchroederNathan/clarity

clarity: an Expo speech practice app where the README and the example environment file disagree about a key

A speech companion app built with Expo

329 stars67 forksTypeScriptMIT

At a glance

What is it?
A React Native app that aligns live transcription onto a reference passage, scores articulation, flow, pacing, fillers and expression, and syncs sessions through Clerk and Convex with local writes first. The configuration tells a sharper story than the feature list: cost constants in environment variables, a fail-closed billing default, and one Azure key named two different ways.
Who is it for?
Clarity is a good reference implementation if you want to see how a speech app handles the awkward parts, namely incremental alignment against a moving teleprompter, an offline-first write path, and keeping a model gateway key out of a mobile bundle. Three things to check before copying any of it.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The Azure Speech key is public in one file and server side in the other

The environment variable table in the README lists the pronunciation assessment keys as public. Both of them.

The example environment file in the same repository lists them without the prefix:

code
AI_COACH_MODEL=google/gemini-3.5-flash-lite
BILLING_ENVIRONMENT=production
ALLOW_APP_STORE_SANDBOX=false
AI_INPUT_USD_PER_MILLION=0.30
AI_OUTPUT_USD_PER_MILLION=2.50
TTS_USD_PER_MILLION_CHARACTERS=15
EXPO_PUBLIC_OBSERVE_IN_DEV=
HUGEICONS_TOKEN=

The file also carries an explicit instruction above the Azure entries: set them on the backend with the environment command, and never prefix provider keys with the public prefix.

So the two documents disagree, and the disagreement is not cosmetic. The prefix rule is stated plainly in both: variables carrying it are compiled into the client bundle. A subscription key to a paid speech assessment service is exactly the kind of credential that should never ship inside a binary that anyone can download.

The README does say the assessment is optional, and that local alignment supplies a fallback score without it, so the safest reading is that the example file is right and the table's prefixes are a mistake. That is still a documentation bug worth catching before somebody copies the table into their own setup.

The rest of the table is consistent with the rule. The Clerk publishable key and the Convex URL are public because the client needs them. The Convex frontend API URL is set on the backend. The AI gateway key is unprefixed. The model, the region, the revenue share and the webhook secret are all unprefixed.

One Expo module sits a major version behind every other one

The stack line says Expo SDK 57, React Native 0.86, React 19 and Expo Router with typed routes. The manifest agrees: the Expo packages are held at `~57.0.x`, React Native is pinned to `0.86.3`, and React is `19.2.3`.

Twenty-nine of those Expo packages carry the tilde. One does not. `expo-speech-recognition` is declared at `^56.0.1`, a full SDK generation behind the rest of the tree, and it is the module the entire scoring pipeline depends on, since it is what streams native transcription while the app records.

The caret on that one line also means it will accept anything from 56.0.1 upward, including a future 57 or 58, if such a release appears. Every other Expo package is held within its own minor.

Elsewhere in the same list the constraints are mixed on purpose and elsewhere by accident. Several packages are exact: the reanimated version, the SVG version, the keyboard controller, the masked view. Most Expo packages use the tilde. Third party libraries use the caret.

The manifest also names a patches directory in the repository, so at least one dependency is modified in place, and it does not declare the package manager that applies those patches. Which dependency needs a patch, and how it survives a dependency bump, is not stated anywhere in the readme.

Copying the example file gives you a fail-closed production billing environment

The setup instruction is to copy the example file to the local environment file and fill in the values. Two of the defaults in that file are traps.

The first is the billing environment. Its value in the example is `production`, and the comment beside it says to set it explicitly to `development` on the development backend, because production is fail-closed by default. So the documented copy step produces a local checkout that is configured to refuse billing operations until somebody remembers to change one line.

The second is the sandbox allowance. Its value is `false`, with a comment saying to enable it for public TestFlight receipts that report as App Store sandbox, that the Test Store remains blocked either way, and to set it back to false before the paid App Store launch.

That variable needs two different values over the life of a launch, true during TestFlight and false at release, and a contributor copying the file gets the second one from the start.

The rest of the file is careful in a way that suggests these were written after a real incident. There is a separate webhook authorization value described as the exact header expected from the RevenueCat webhook, distinct from the RevenueCat secret API key. There is a note that platform keys are required for release builds, because without one purchases stay disabled rather than falling back to a test key, which the comment says RevenueCat crashes release builds for deliberately. And there is a note that the public platform keys are the app specific ones and never a secret key.

Model, voice and revenue costs are all environment variables

The example file is as much a unit economics sheet as a configuration file.

There is a coaching model default, with the comment that it is optional and falls back to a specific small model identifier. Then a per million token input price, a per million token output price, and a per million characters price for the voice. Then an Azure assessment cost per hour field. Then a revenue share field, described as actual net proceeds over gross receipts in United States dollars, including applicable store and RevenueCat fees.

Those numbers are inputs to a calculation the app performs, not claims about what a vendor charges, and the comments make that distinction by describing the denominators rather than the rates.

The presence of the revenue share variable alongside the per unit prices is the interesting part. It is the only place in the repository where the commercial terms of the product are expressed as configuration rather than as prose, which means the entitlement decision and the cost estimate are reading the same numbers.

The generated features are what the model key buys. The README says the key enables coaching, generated word practice passages and model pronunciation audio, and it is kept out of the app bundle by routing those calls through the router's API routes on the server directory. The result screen streams a short summary and three tips grounded in the session's measurements, and a running list of difficult words can be turned into a generated practice passage.

So there is one model-backed path that is required for three separate features and one shared key behind them.

Two test account credentials are prefixed for the client bundle

The local QA row of the environment table lists three variables, and all three carry the public prefix: a mock practice flag, a development sign-in email, and a development sign-in password.

By the project's own rule, those three are compiled into the client bundle. A test account's email address and password baked into a shipped binary is a different category of problem from a mock flag, and the file marks all three as optional with no distinction drawn between them.

The simulator testing section raises the stakes slightly. The iOS simulator does not give reliable speech input, so either the mock flag is set or the end to end build's simulator profile is used. That profile also turns on the QA only sign-in and seeds history, while disabling over the air updates so that a shared preview update cannot replace the test bundle.

Disabling updates for a QA profile is a good call and it does not change the fact that the credential variables are readable from the binary.

The third variable in that group, the debug telemetry flag, is handled more carefully. Its comment says to set it to one to send metrics from a debug build, that it is off by default so development noise stays out of the dashboard, and that it has no effect on release builds. That last clause is a claim the file makes about behaviour rather than about the bundler, and it is the kind of claim worth testing on a release artefact.

Expo Go cannot load any of it, so there is no quick start

Clarity requires a development build, and the reason is stated: Expo Go cannot load the native modules used by speech recognition, the synchronous key value store, and the purchase SDK. Those three are exactly the features the app is built around.

That has consequences for anyone evaluating the project. There is no way to open it in a stock client, so following the repository means a full native toolchain, either Xcode or Android Studio, a Hugeicons token for the private icon packages, and matching Clerk and Convex development projects.

Then there is the hardware requirement. A physical device is needed to test microphone and speech behaviour, and the list of things that only work on hardware is specific: microphone permissions, audio routing, transcription quality, the Azure assessment, and recording playback. On the simulator the documented workaround is deterministic mocked practice or the dedicated build profile.

The order of operations is also prescribed. The local environment file has to exist before installing dependencies, because the package manager reads it during installation and the icon token is referenced from the npm configuration. The Convex watcher runs in one terminal and creates or links a development deployment on its first run, writing the deployment URL and identifier into the local file. Then the native build runs in a second terminal.

One step is called out as easy to get wrong. The frontend API URL has to be set on the backend rather than in the local file, using the environment command, and the value points at the matching Clerk instance.

Five places for agent instructions and two Expo configuration files

The root of the repository is unusually crowded with tooling configuration.

There are three dot directories that belong to different agent harnesses, one for a generic agents setup, one for Claude, one for another. There are two instruction files at the root by name, one generic and one Claude specific. There is a separate directory whose name describes code review configuration. And a commit hook configuration in its own directory.

None of them is described in the readme.

There are also two Expo configuration files, a TypeScript one and a JSON one. The TypeScript file is the one the tooling reads, and the readme says nothing about the JSON file, whether it is a fallback, a base, or a leftover.

The rest of the root has the expected pieces plus two that are not. There is the app directory, components, constants, the Convex functions, documentation, the end to end build configuration, scripts, a server directory for the API routes, services, shared state, types, a web target, and a patches directory.

There is also a temporary directory and an output directory, both at the root of a versioned repository. Neither appears in the readme and neither is obviously ignored, so a build that writes there leaves artefacts in the working tree.

A store configuration file for Apple platforms and a preview image sit alongside them, and the lockfile is Bun's, consistent with the documentation telling you to use Bun for installation and every command in the commands table being a Bun invocation.

Version 1.0.0, no release, and two domains

The manifest says version 1.0.0 and the repository has no GitHub releases. The only distribution channel named is a TestFlight link, described as joining the iOS beta.

That combination is worth holding onto. A 1.0.0 version number on a project distributed only through a beta channel, with no tag to diff against, means a checkout is identified by a commit and nothing else.

Two domains are in play. The repository record lists a site at one address, and the readme links a different address for the website. Neither is documented, and one of them is on a hosting provider's default domain pattern rather than a registered name.

The commands table lists eight suites that the test command runs: history, sync, statistics, alignment, scoring, waveform, entitlement and settings. That list is a good description of what the app actually does, since it names the hard parts, the incremental aligner and the entitlement checks among them.

The scoring model is stated in the features rather than the commands. One score out of a hundred, five skill scores for articulation, flow, pacing, fillers and expression, and for scripted sessions a word by word breakdown plus recording playback. The transcription path handles skipped, repeated, inserted and partially spoken words while the teleprompter keeps moving, which is the specific reason an incremental aligner exists rather than a batch one.

Results are written to local storage first and pushed to the backend afterwards through a single bridge, so the first screen does not wait on the network.

Editorial conclusion

Clarity is a good reference implementation if you want to see how a speech app handles the awkward parts, namely incremental alignment against a moving teleprompter, an offline-first write path, and keeping a model gateway key out of a mobile bundle. Three things to check before copying any of it. Follow the example file literally and you will get a production billing environment on your laptop, two test-account credentials get compiled into the client if you set them, and the two files in the repository disagree about whether the Azure Speech key belongs in the bundle at all.

Frequently asked questions

What does the Clarity speech practice app do?

It is an Expo app for iOS and Android that reads a passage or takes a freestyle prompt, streams native transcription while recording, aligns that transcript onto the reference text incrementally, and scores the session out of 100 plus five skills: Articulation, Flow, Pacing, Fillers and Expression. Scripted sessions also get a word by word breakdown and playback.

Can I run the Clarity app in Expo Go?

No. It requires a development build, because Expo Go cannot load the native modules used for speech recognition, MMKV storage and RevenueCat purchases. A physical device is also needed for anything involving the microphone, audio routing, transcription quality, the Azure assessment or playback.

Does Clarity need an Azure Speech key to score a session?

No, it is optional. When Azure credentials are available the app adds word, syllable, phoneme, fluency and prosody detail. Without them the local incremental alignment still produces a complete fallback result. The two files disagree on whether those variables carry the public prefix, and the example file explicitly says never to prefix provider keys with it.

Where does Clarity store a session before it syncs?

On device first. The result is written to MMKV synchronously, and a single sync bridge pushes local records to Convex after Clerk authenticates the account. That is why the readme says the first screen does not have to wait on the network.

How does Clarity keep the AI Gateway key out of the app?

By routing the generated features through Expo Router API routes on the server. The key is configured without the public prefix, which the readme explains is required because prefixed variables are compiled into the client bundle. The key enables coaching, generated practice passages and model pronunciation audio.

Official sources

  1. Issues
  2. License: MIT
  3. Project website
  4. README
  5. SchroederNathan/clarity on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/schroedernathan-clarity.svg)](https://hysenlabs.com/projects/schroedernathan-clarity)