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
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 openYour server (or curl) then sends through the relay:
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
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), andPush.getLaunch()(the notification that cold-started this run). - A payload's
urldoubles as a deep link: it is also delivered throughapp.launchUrl/ the app"url"event, so link-routing apps get notification routing without touchingPush.
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).
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:
lecodes pn devices # did the device actually register?
lecodes pn send --title "Order shipped" --user u42For 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:
lecodes pn keys new "prod server" --env # writes LECODES_PUSH_KEY= into .envor the project's settings → Push notifications → New 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.
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
- Geolocation — the service-plugin pattern this follows
- app —
app.launchUrl+ the"url"event (deep-link path) - Conventions — host gating