Registration — app identity

Every Civility app has an opaque app id and an optional manifest. Registering them brands the consent popup and locks token delivery to your origin. It’s optional — users can approve unregistered apps — but registration is what unlocks the trust signals below.

Identity model, origin guard, and verification ladder: app identity spec.


1. The app id #

A unique, immutable string that identifies your app across all servers. civ init generates one automatically (app- + 24 base32 chars); for manual setups, civ id mints one.

Do not regenerate. Every grant, token, and sync path binds to this id. Regenerating orphans all user grants.

The id is public — it ships in your static bundle, like an OAuth client_id. Authority comes from the user-granted token, not from the id being secret.


2. The manifest #

A JSON document that describes your app to the consent popup. The popup shows your app’s name, icon, and requested permissions when a user authorizes.

{
  "name": "My App",
  "description": "A short description of the app",
  "homepageUrl": "https://myapp.example",
  "requestedPermissions": [
    { "store": "*", "level": "read" }
  ]
}

| Field | Required | Notes | | ––––––––––– | –––– | —————————————————————————————————————————————— | | name | yes | Shown in the consent popup. | | description | yes* | Shown in the consent popup. | | requestedPermissions | yes* | Store permissions the app requests. * = all stores. | | homepageUrl | no | Origin used for postMessage delivery when opener origin is unknown. | | iconUrl | no | Icon shown in the consent popup. | | requiresEncryption | no | Server rejects every plaintext push from this app (§4.8). A one-way door — decide before you ship. | | services | no | CivServices scopes the app requests — see Services. | | exposes / requests | no | Cross-app read-only replicas (§3.9.5). | | changeRetentionDays | no | Override how long the server keeps this app’s change history. |

* Required in the AppManifest TypeScript type, which is what you write in www/store/app.ts. The wire schema defaults both to empty, so hand-written JSON may omit them. homepageUrl and iconUrl must be http(s) URLs — other schemes are rejected at the schema boundary.


3. Register #

From your app (the usual path)

Pass the manifest to your CloudStore and it registers on first sync — no separate step, and the manifest ships with the code it describes. This is what civ init scaffolds:

export const APP_MANIFEST: AppManifest = { name: 'My App' /* … */ }

const cloud = new CloudStore(backend, {
  // …
  cloud: { appId: APP_ID, manifest: APP_MANIFEST },
})

Via the dashboard

  1. Go to Dashboard → Developers → Register an App.
  2. Enter your app id and paste the manifest JSON.
  3. Submit. The manifest is validated against the shared schema before the request is sent.

Via the API

curl -X POST /api/v1/apps \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{"id": "app-…", "manifest": { "name": "My App", ... }}'

Quick create (no manifest)

Create a bare app id without a manifest — useful during development. Register a manifest later when you’re ready to ship.


4. Verification #

✓ Verified in the popup means the app’s origin published its id back. The server checks this live at authorize time: the app announces its id from an origin, and that origin’s /.well-known/civility-app.json lists the same id. Mismatch → unverified, still functional, just unlabeled.

Publish the well-known file

civ build emits this automatically. For manual setups, serve this at https://yourapp.example/.well-known/civility-app.json:

{
  "appId": "app-9f8a2c…",
  "name": "My App",
  "homepageUrl": "https://yourapp.example"
}

A single origin hosting multiple apps can list several ids:

{
  "appIds": ["app-9f8a2c…", "app-abc123…"],
  "name": "My Platform",
  "homepageUrl": "https://platform.example"
}

DNS TXT alternative

If you can’t add a well-known route but control DNS:

_civility-app.yourapp.example  TXT  "civility-app-id=app-9f8a2c…"

The well-known file is tried first; DNS is a fallback.


5. Origin guard #

Each grant records the origin it was authorized from. If the same app id arrives from a different origin, the user is shown a re-consent prompt — even when no new permissions are requested. Approving stamps the new origin onto all active grants.

This prevents impersonation (a copied id surfaces the attacker’s true origin) and supports legitimate domain moves (one honest re-consent carries grants over).


Next #

  • Sync — replicate data across devices using your registered app.
  • Store — local state, the foundation every app builds on.
  • Services — call AI and other services from your app.