Security and privacy.
Where keys live, who can read what.
How Muse AI stores your provider keys, what happens if the encryption key is lost or changed, who can open the console, what moderation checks and what it does not, what the backend stores about users and what it never stores. Read it before you go live and before you write your privacy policy.
01Secrets at rest
A key is like a password for your provider account: anyone who has it can spend your money. A key you enter in the console exists in two places:
| Place | How it is protected |
|---|---|
The console's own storage (data/admin.sqlite, or Postgres with DATABASE_URL) | Encrypted with AES-256-GCM using ADMIN_SECRET_KEY, with the setting's name bound to each value. The key is never shown again in the console, never sent to the browser, and appears in the audit log only as "set" or "cleared". Settings exports never contain secrets. |
Firestore config/private | Written by the console with the service account and read only by the backend. Firestore security rules deny every app and browser access to config/*; only server credentials (the Functions runtime and your service account) can read it. Google encrypts Firestore data at rest on its side. Anyone with Owner or Editor rights on the Google Cloud project, or with the service-account key file, can read it: keep that list short. |
The app never receives a key. config/public, which the app sees through GET /v1/config, never contains one.
02If ADMIN_SECRET_KEY is lost or changed
The console cannot decrypt the stored keys without the exact ADMIN_SECRET_KEY that encrypted them. There is no recovery: nobody, including the author, can read them without it.
What happens if the console starts with a different or missing key:
- The log prints
[admin] secret "ai.anthropicKey" cannot be decrypted: was the secretKey changed?for each stored key. - The console then refuses to sync: it logs
[admin] sync: … stored secret(s) cannot be decrypted (…): ADMIN_SECRET_KEY is not the key they were saved with. config/private was left unchanged.and leavesconfig/privateas it was, so the app keeps working with the keys already there. Saving settings fails the same way until you fix the key.
Recovery: put the original ADMIN_SECRET_KEY back and restart; the next sync writes the keys again. If the original is gone, set the new key, then enter every secret again (AI keys, image keys, compatible endpoint keys, store keys, reporting tokens, webhook secret) with Replace and save. Your provider dashboards let you create new keys at any time.
If you set no ADMIN_SECRET_KEY at all, the console generates data/.admin-secret-key and warns. That works on one server, but the key then lives next to the data it protects; set your own in the environment for production.
03Rotating keys
- Provider keys: create a new key at the provider, Console → the key → Replace → Save changes → Test connection, then revoke the old key at the provider. The backend reads the new key from
config/privateon its next config load. - Service-account key of the console: create a new key in Google Cloud → IAM → Service accounts → Keys, update
GOOGLE_APPLICATION_CREDENTIALS, restart, delete the old key. - ADMIN_SECRET_KEY itself: there is no automatic re-encryption. Change it only together with re-entering every secret, as described above.
04Backups
ADMIN_SECRET_KEY, kept separately from the data (a password manager).- The console's
data/folder (or your Postgres database): team, settings, history, audit log and the encrypted keys. - A settings export (Console → Export / Import): every setting except secrets, for moving to a new install.
- Firestore: use Google Cloud's scheduled Firestore backups or exports if you need to restore user entitlements and purchase records.
- Your Android upload keystore and
key.properties, and the App Store.p8key file.
05Console access
- Passwords are hashed with scrypt; sessions and the setup code are stored only as hashes.
- Two-factor with an authenticator app for every account (Security & alerts), and a team rule to require it for owners and managers.
- Roles: viewer (read), manager (settings and product pages), owner (keys, store identifiers, team, export).
- Every change is in the append-only audit log, with a reason where one is required.
- The console's own pages send a strict Content-Security-Policy (scripts and styles only from the console itself).
- Serve the console only over HTTPS. Treat the service-account key like a password: it bypasses Firestore rules.
- User search is by user id only; the console has no screen that links a user to their conversations.
06Privacy: what is stored about users
Conversations stay on the device (sqflite on phones, browser storage on the web). The backend forwards a message to your AI provider and stores nothing of its text, prompts, attachments or replies, unless the user turns on cloud backup. Function logs carry ids, model, token counts and status only.
| Stored on your Firebase project | Why |
|---|---|
users/{uid}: created and last-seen time, platform, language, age confirmation, plan (store and console grant), Remove ads, credits, ban flag | Plans, allowance and entitlements |
Push tokens (users/{uid}/devices) | Push campaigns |
Usage counts per user (usage, daily and monthly, kept 35 days) and totals per day (stats) | Limits and the console's counters |
Generated and edited images and AI videos, with their prompts (images, videos, Storage) | The user's own gallery across devices; the user can delete them |
| Discover items: headline, link, a summary of at most 300 characters from the feed, picture link, date; cached briefs | The Discover tab (not per user; removed after "Keep items for n days") |
Reported content (reports) | Only what the user explicitly reports, for your review |
| Verified purchases and revenue events | Entitlements and your financial records |
The optional chat backup (users/{uid}/sync) | Only if the user signs in and turns on "Back up chats"; only the user can read it |
Not stored: chat messages (without backup), attachments, audio sent for transcription, text sent to natural voices, and anything typed in incognito chats. What your providers do with the data they receive is governed by their own terms; name your providers in your privacy policy.
07Account deletion
Settings → Delete account calls DELETE /v1/me. The API deletes users/{uid} with its devices and backup, all usage documents, every generated or edited image and every AI video (file and record), reward tokens and rate-limit windows, then the Firebase Auth user; the app removes its local data. It keeps purchases and revenueEvents (financial records; the uid stays as an opaque id) and reports (the reported content stays for your review; the reporter becomes "deleted"). Say so in your privacy policy.
08Moderation scope
Moderation is a filter that runs on your server before a request reaches a provider. Know exactly what it covers, so your store answers and your policies are accurate.
| Checked | How |
|---|---|
| Chat messages (the last message, its text files and the custom instructions) | Your blocklist, then the optional OpenAI moderation endpoint |
| Image prompts, image-edit prompts, AI video prompts | Your blocklist, the locked rule that refuses prompts combining a minor term with a sexual term (no switch), then the optional OpenAI endpoint |
| Text sent to natural voices | Your blocklist, then the optional OpenAI endpoint |
| Everything sent to a provider | The provider's own safety systems |
| Answers and images users report | Your review in Console → Reports |
| Not checked by Muse AI's moderation | What applies instead |
|---|---|
| Uploaded images and attachments (photos, PDFs), the input picture of an edit | The provider's own safety |
| Remove background and upscale | The provider's own safety (there is no prompt) |
| Transcription audio and its text | None by Muse AI |
| Discover feed text (headlines and summaries from publishers) | Your choice of feeds; briefs are written by your chat provider |
A blocked request answers 422 blocked, is not charged, and its text is not stored; only a count is kept. If the moderation endpoint itself fails, the request continues with the provider's own safety. Settings: Console → Moderation.
09The age check
On first launch the app asks the user to tick "I am 17 or older" (the number is your Minimum age: 13, 16, 17 or 18). This is a self-confirmation checkbox: the app records that the user confirmed, and nothing more. It does not verify age, check documents or estimate age. Set your store age ratings accordingly and do not call it age verification in your listing, privacy policy or store answers.
10Web build and Content-Security-Policy
If you publish the web build, serve it with a strict Content-Security-Policy. The author's demo host sends one without 'unsafe-inline' and without 'unsafe-eval': the app is built with --csp --no-web-resources-cdn, the policy allows 'wasm-unsafe-eval' for CanvasKit, two hashes for Flutter's injected style blocks and fonts.gstatic.com for fallback fonts, and sets frame-ancestors 'none'. The full header is on Deployment → web build.
11Safety rules in the code
- Image, edit and video prompts combining a minor with sexual content are always refused, with no setting to turn it off.
- AI output is always labelled (composer notice, image badge and file metadata).
- No AI, voice, video, ad or store key ships in the source;
tool/check_ad_ids.shproves it for ad ids. - Every module that calls a paid provider and every money switch ships off.