Skip to Content
AIAI Consent

AI consent

NativeExpress asks each user for permission before chat, create or scan sends anything to a third-party AI, which the App Store requires of every app that does this.

Adapt the AI consent card in this project to my app. Read supabase/functions/_utils/consent.ts, src/lib/aiConsent.ts and the ai_consent keys in src/i18n/en.json and de.json. Make the card say concretely what each of my AI features sends and name every recipient, keep en and de at key parity, and tell me whether my change needs consentVersion bumped in supabase/functions/_utils/ai.config.json.

A user who has not allowed it sees an inline card where they are about to send: in place of the chat composer, in place of the Generate button, and at the top of the scan screen before the camera opens. The card says what that feature sends and who receives it, for example:

To answer, Native Express sends the text you type, any photos you attach and the earlier messages in that chat to OpenRouter, which passes it to OpenAI, Anthropic or Google, depending on the model.

Allow records the permission on the account and the request goes ahead. What’s shared opens a sheet listing every AI feature, what it sends and to whom. One permission covers all AI features. The user withdraws it under Profile → AI data sharing, and the next AI request asks again.

Until the user allows it, Scan disables its capture button and hides its other capture actions, and Create hides Regenerate on a result. If the server refuses a request anyway, for example because the permission was withdrawn on another device, the card comes back, and Allow resends the message, prompt or photo that was refused.

Why the App Store requires it

App Review Guideline 5.1.2(i)  has applied since 13 November 2025. An app must clearly disclose where personal data is shared with a third-party AI and get the user’s explicit permission before the first transmission. Explicit means an affirmative tap such as Allow. Accepting your terms at sign-up is not enough, and apps are rejected for relying on it.

Chat sends message text and attached photos, create sends the image description, and scan sends the photo. All three go to OpenRouter, which forwards them to the company whose model answers. So every app that keeps one of these features needs the consent, and the boilerplate ships it in place.

Change the wording

To changeEdit
The card and the sheetThe ai_consent keys in src/i18n/en.json and src/i18n/de.json
What a feature says it sendschat.ai_consent_sent, create.ai_consent_sent, scan.ai_consent_sent in both files
The Profile row and its withdraw dialogThe profile.ai_consent* keys
How a provider is namedPROVIDER_NAMES in src/lib/aiConsent.ts, keyed by the model id’s prefix

The recipients are not written into the copy. The card reads the model ids each feature can send to and names OpenRouter plus the maker of each model, so adding a model to supabase/functions/chat/config.json from a new provider adds that provider to the card. The app name comes from general.appName in config.js.

Keep the text concrete. Apple wants the data named (“the photo you take”, not “your content”) and the recipient named (“OpenRouter, which passes it to Google”, not “our AI partners”).

Ask everyone again

Bump consentVersion in supabase/functions/_utils/ai.config.json when the data a feature sends or the companies receiving it change: a model from a new provider, a feature that starts sending location or contacts, a new AI feature. A user’s permission counts only for the version they allowed, so everyone sees the card again.

Bump the version

supabase/functions/_utils/ai.config.json
{ "freeMessagesPerMonth": 10, "consentVersion": 2 }

The app reads the same file through config.js, so the build and the functions agree on the number.

Ship the app first

Release the build containing the new version. Until it is live, deployed functions on the old version keep accepting users who allowed either one.

Deploy the functions

supabase functions deploy chat generate identify

Users still on an older build get a card asking them to update the app the next time they send, rather than an Allow the server would refuse.

Verify

Open chat with an account that allowed the previous version. The card appears again, and after Allow the message sends.

Add an AI feature

A new feature that sends user content to a model needs all three parts, or it either skips the question or fails without explaining why:

  1. Export an aiDisclosure from the feature’s config.ts, with sentKey, the i18n key describing what it sends, and modelIds, the models it sends to. Set it on the feature in feature.ts. The sheet and the Profile row read it through aiFeatures, exported from @/features.
  2. Render AiConsentCard from @/components/ai-consent/AiConsentCard where the user sends, and hold back the send controls while useAiConsent().isRequired is true. When a request fails with ai_consent_required, pass the error to handleRefusal(error). It reads the version the server wants from the error.
  3. Call assertAiConsent(user) from supabase/functions/_utils/consent.ts in the edge function, straight after authenticateUser and before any quota or model call.

Gotchas

  • The App Review demo account must not have allowed AI. The reviewer needs to see the card. If you tested with that account, withdraw it under Profile first.
  • The functions refuse even when a screen skips the card. A rewritten screen that drops the card still cannot reach a model, because each function refuses without consent. The user then sees an error instead of the card, which is a rejection of its own.
  • Google’s “shared” answer depends on your OpenRouter settings. The boilerplate declares the AI providers as processors, not sharing. That holds only while your OpenRouter account refuses providers that may train on inputs. See Privacy declarations.
  • Maestro flows grant the permission. The chat, create, scan and screenshot flows tap Allow when the card shows, so run them with a test account. See Testing.
  • Removing features keeps consent. It is shared infrastructure, like the quota. Removing chat leaves the card on create and scan. With every AI feature removed there is no card and no Profile row.

Error codes

CodeHTTPMeaning
ai_consent_required403The user has not allowed AI processing, withdrew it, or allowed an older consentVersion. The body carries the version the server wants.

The app answers this code with the consent card, not an error. When the version in the body is newer than this build knows, Allow could never satisfy the server, so the card asks the user to update the app instead.

Files

        • AiConsentCard.tsx - The inline card with Allow
        • AiConsentSheet.tsx - What's shared
        • AiConsentRow.tsx - Profile state and withdrawal
      • useAiConsent.ts - Gate, grant, withdraw, server refusals
      • aiConsent.ts - Version, account record, recipient names
      • */config.ts - Each feature's aiDisclosure
    • ai.config.json - consentVersion
    • consent.ts - assertAiConsent

How it works

The permission is stored on the account as user_metadata.ai_consent, so it survives a reinstall and follows the user to another device. The functions read it through auth.getUser, which asks Supabase Auth rather than trusting the claims in the access token, so a withdrawal counts on the next request. user_metadata is writable by the user it belongs to, which is appropriate here: the record is that user’s own answer.

The check runs before quota, storage or provider work, so a refused request costs the user nothing from their monthly allowance.

ai_consent_granted and ai_consent_withdrawn go to analytics with the version as their only property.

Last updated on