LeCodesdocs

Push

Remote push notifications. Like Geolocation this is a service plugin — a typed wrapper over the host-registered "push" service — but most of the machinery lives outside your app: the host owns the OS token, the permission prompt, and the registration with the le.codes backend; le.codes relays your sends to APNs/FCM, so you never touch Apple/Google credentials in the default (LeCodes app) setup. Host-optional — always gate on Push.isSupported.

At a glance

TypeScript
if (Push.isSupported) {
  const { address } = await Push.register({ user: 'u42' })
  // hand `address` to your backend — or skip it entirely and target `user` ids
}
Push.addEventListener('message', p => toast(p.title))   // arrived while the app is open

Your server (or curl) then sends through the relay:

TypeScript
POST https://le.codes/api/projects/<projectUuid>/push/send
Authorization: Bearer lecodes_pk_…        ← a project push key le.codes minted for you (see below)
{ "title": "Order shipped", "users": ["u42"], "url": "myapp://orders/42" }

API

TypeScript
Push.isSupported: boolean                   // did this host register the service?

Push.getStatus(): Promise<PushStatus>       // never prompts
Push.register(options?: { user?: string }): Promise<{ address: string }>
Push.unregister(): Promise<void>
Push.getLaunch(): Promise<PushPayload | null>
Push.addEventListener('message' | 'tap', cb: (p: PushPayload) => void): void
Push.removeEventListener('message' | 'tap', cb): void

interface PushPayload {
  title: string
  body?: string
  url?: string                    // deep link — also rides app.launchUrl / the "url" event
  data?: Record<string, any>      // your JSON, ≤ 2 KB serialized
  badge?: number
}
interface PushStatus {
  permission: 'granted' | 'denied' | 'prompt'
  registered: boolean
}
  • The OS permission prompt happens inside register() — never earlier. register() is idempotent (safe every launch) and rejects "denied" / "unavailable". On hosts without the service every method rejects immediately, before any permission path.
  • Your app is not running when a push arrives — the OS displays it. The app sees exactly three delivery paths: the "message" event (arrived while your app was open — no banner), the "tap" event (tapped while your app ran), and Push.getLaunch() (the notification that cold-started this run).
  • A payload's url doubles as a deep link: it is also delivered through app.launchUrl / the app "url" event, so link-routing apps get notification routing without touching Push.

Identifying YOUR users

register({ user }) tags the device with your app's own user id, and the send API accepts users: ["u42"] — one user fans out to all their devices, and your backend never stores addresses. Re-registering without user clears the tag (logout).

Note

the user id is client-asserted — any device running your app could claim any id and also receive that user's notifications. Fine for "order shipped"; never target secrets by user id.

Sending

While developing you need no credential at all — the CLI sends on your login:

TypeScript
lecodes pn devices                                  # did the device actually register?
lecodes pn send --title "Order shipped" --user u42

For your own server, use a project push key (lecodes_pk_…). le.codes generates it — you can't choose one, and it's shown exactly once, so capture it at creation:

TypeScript
lecodes pn keys new "prod server" --env             # writes LECODES_PUSH_KEY= into .env

or the project's settings → Push notificationsNew key (owner only; the panel also hands you the ready-made request). A key can ONLY send pushes for its own project — deliberately useless for anything else, unlike a personal access token, which is why it's the thing to deploy. Lost keys aren't recoverable: mint a new one and revoke the old. Personal tokens with developer access also work on this route (that's what lecodes pn send uses).

Targeting: to: [address…], users: [id…], or both — response { sent, failed: [{address, reason}] }. Unknown addresses reject the whole batch; a user with no devices is just zero sends.

Note

push needs an attributed world — the LeCodes app knows which project it is running only when a trusted launcher declared it (QR / link / onboarding launches of published projects). lecodes dev bundles are unattributed, so Push.register() rejects "unavailable" there — test push through a published project opened via QR.

Your own app (standalone shell)

The same bundle and the same code work in a lecodes app shell: the shell registers the same "push" service with its own bundle id and your own APNs key (uploaded to project settings), and your server sends the exact same request — the relay picks credentials per registration.

See also