← All @molecule/* packages · App templates
@molecule/api-resource-deviceAPI resource · auth · API (Node) · v1.0.1 · Apache-2.0
Device resource types and schema for molecule.dev.
npm install @molecule/api-resource-device@molecule/api-resource-device is an API resource: the routes, validation and storage for auth, built on the database and auth cores so it runs on whichever providers your app has bonded.
import { createRequestHandler } from '@molecule/api-resource'
import { createRequestHandlerMap, resource, routes } from '@molecule/api-resource-device'
// Unlike newer resources, the handler map is a FACTORY — build it with the
// createRequestHandler from @molecule/api-resource (mlcl inject does this):
const requestHandlerMap = createRequestHandlerMap(createRequestHandler)Works with: @molecule/api-bond, @molecule/api-database, @molecule/api-i18n, @molecule/api-locales-device, @molecule/api-resource
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
The device resource for molecule.dev — one row per signed-in browser or
app install, owned by a user. Devices anchor session context and push
delivery: the row stores pushPlatform/pushSubscription, the routes cover
device list/read/update/delete plus GET /devices/push/public-key (the
VAPID key browsers need to subscribe), and API-side push fan-outs read the
stored subscriptions.
import { createRequestHandler } from '@molecule/api-resource'
import { createRequestHandlerMap, resource, routes } from '@molecule/api-resource-device'
// Unlike newer resources, the handler map is a FACTORY — build it with the
// createRequestHandler from @molecule/api-resource (mlcl inject does this):
const requestHandlerMap = createRequestHandlerMap(createRequestHandler)
resource
npm install @molecule/api-resource-device @molecule/api-bond @molecule/api-database @molecule/api-i18n @molecule/api-locales-device @molecule/api-resource zod
DeviceRequestHandlerMapShape of the device request-handler map produced by createRequestHandlerMap.
Names match the route definitions in routes.ts. Exported so helpers that
accept the map (e.g. mountDefaultDeviceRoutes) can type their parameter
precisely instead of widening to Record<string, MoleculeRequestHandler>.
interface DeviceRequestHandlerMap {
auth: MoleculeRequestHandler
authUser: MoleculeRequestHandler
del: MoleculeRequestHandler
pushPublicKey: MoleculeRequestHandler
query: MoleculeRequestHandler
read: MoleculeRequestHandler
update: MoleculeRequestHandler
}
APNPushSubscriptionAPN push subscription type.
type APNPushSubscription = z.infer<typeof apnPushSubscriptionSchema>
CreatePropsFields required when registering a new device (userId, name, push subscription).
type CreateProps = z.infer<typeof createPropsSchema>
FCMPushSubscriptionFCM push subscription type.
type FCMPushSubscription = z.infer<typeof fcmPushSubscriptionSchema>
PropsFull device record properties (userId, name, push platform/subscription, timestamps).
type Props = z.infer<typeof propsSchema>
PushPropsPush notification properties for a device (device ID, platform, subscription data).
type PushProps = z.infer<typeof pushPropsSchema>
PushSubscriptionWeb Push subscription type.
type PushSubscription = z.infer<typeof webPushSubscriptionSchema>
UpdatePropsUpdatable device fields (name, push platform, push subscription).
type UpdateProps = z.infer<typeof updatePropsSchema>
createRequestHandlerMap(createRequestHandler)Creates the full request handler map for the Device resource. Maps handler names (matching
route definitions) to Express middleware: auth, authUser (authorizers), and del, query,
read, update (CRUD handlers).
function createRequestHandlerMap(
createRequestHandler: (
handler: Handler,
) => (req: MoleculeRequest, res: MoleculeResponse, next: MoleculeNextFunction) => Promise<void>,
): DeviceRequestHandlerMap
createRequestHandler — Factory from @molecule/api-resource that wraps handler configs into Express middleware.Returns: A DeviceRequestHandlerMap of handler names to Express middleware.
apnPushSubscriptionSchemaAPN push subscription schema.
const apnPushSubscriptionSchema: z.ZodObject<{ registrationId: z.ZodString }, z.core.$strip>
createPropsSchemaSchema for creating a device.
const createPropsSchema: z.ZodObject<
{
userId: z.ZodString
name: z.ZodOptional<z.ZodString>
pushPlatform: z.ZodOptional<z.ZodEnum<{ fcm: 'fcm'; apn: 'apn' }>>
pushSubscription: z.ZodOptional<
z.ZodNullable<
z.ZodUnion<
readonly [
z.ZodObject<
{
endpoint: z.ZodOptional<z.ZodString>
keys: z.ZodOptional<
z.ZodObject<{ p256dh: z.ZodString; auth: z.ZodString }, z.core.$strip>
>
},
z.core.$strip
>,
z.ZodObject<
{ registrationId: z.ZodString; registrationType: z.ZodLiteral<'FCM'> },
z.core.$strip
>,
z.ZodObject<{ registrationId: z.ZodString }, z.core.$strip>,
]
>
>
>
hasPushSubscription: z.ZodOptional<z.ZodBoolean>
},
z.core.$strip
>
deviceServiceDeviceService implementation for the bond system.
Provides device CRUD operations that other resources
can use through get('device') / require('device').
const deviceService: DeviceService
fcmPushSubscriptionSchemaFCM push subscription schema.
const fcmPushSubscriptionSchema: z.ZodObject<
{ registrationId: z.ZodString; registrationType: z.ZodLiteral<'FCM'> },
z.core.$strip
>
i18nRegisteredThe i18n registered.
const i18nRegistered: true
propsSchemaThe full schema for device props.
const propsSchema: z.ZodObject<
{
id: z.ZodString
createdAt: z.ZodString
updatedAt: z.ZodString
userId: z.ZodString
name: z.ZodOptional<z.ZodString>
pushPlatform: z.ZodOptional<z.ZodEnum<{ fcm: 'fcm'; apn: 'apn' }>>
pushSubscription: z.ZodOptional<
z.ZodNullable<
z.ZodUnion<
readonly [
z.ZodObject<
{
endpoint: z.ZodOptional<z.ZodString>
keys: z.ZodOptional<
z.ZodObject<{ p256dh: z.ZodString; auth: z.ZodString }, z.core.$strip>
>
},
z.core.$strip
>,
z.ZodObject<
{ registrationId: z.ZodString; registrationType: z.ZodLiteral<'FCM'> },
z.core.$strip
>,
z.ZodObject<{ registrationId: z.ZodString }, z.core.$strip>,
]
>
>
>
hasPushSubscription: z.ZodOptional<z.ZodBoolean>
},
z.core.$strip
>
pushPropsSchemaZod schema for push notification properties (device ID, platform, subscription).
const pushPropsSchema: z.ZodObject<
{
id: z.ZodString
pushPlatform: z.ZodOptional<z.ZodEnum<{ fcm: 'fcm'; apn: 'apn' }>>
pushSubscription: z.ZodOptional<
z.ZodNullable<
z.ZodUnion<
readonly [
z.ZodObject<
{
endpoint: z.ZodOptional<z.ZodString>
keys: z.ZodOptional<
z.ZodObject<{ p256dh: z.ZodString; auth: z.ZodString }, z.core.$strip>
>
},
z.core.$strip
>,
z.ZodObject<
{ registrationId: z.ZodString; registrationType: z.ZodLiteral<'FCM'> },
z.core.$strip
>,
z.ZodObject<{ registrationId: z.ZodString }, z.core.$strip>,
]
>
>
>
},
z.core.$strip
>
pushSubscriptionKeysSchemaWeb Push subscription keys schema.
const pushSubscriptionKeysSchema: z.ZodObject<
{ p256dh: z.ZodString; auth: z.ZodString },
z.core.$strip
>
pushSubscriptionSchemaCombined push subscription schema (union of all types).
const pushSubscriptionSchema: z.ZodOptional<
z.ZodNullable<
z.ZodUnion<
readonly [
z.ZodObject<
{
endpoint: z.ZodOptional<z.ZodString>
keys: z.ZodOptional<
z.ZodObject<{ p256dh: z.ZodString; auth: z.ZodString }, z.core.$strip>
>
},
z.core.$strip
>,
z.ZodObject<
{ registrationId: z.ZodString; registrationType: z.ZodLiteral<'FCM'> },
z.core.$strip
>,
z.ZodObject<{ registrationId: z.ZodString }, z.core.$strip>,
]
>
>
>
resourceThe device resource definition.
const resource: types.Resource<unknown>
routesDeclarative route definitions for the Device resource, used to generate the Express router.
const routes: (
| { method: 'get'; path: string; middlewares: string[]; handler: string }
| { method: 'patch'; path: string; middlewares: string[]; handler: string }
| { method: 'delete'; path: string; middlewares: string[]; handler: string }
)[]
updatePropsSchemaZod schema for updating a device (partial pick of name, push platform, push subscription).
const updatePropsSchema: z.ZodObject<
{
name: z.ZodOptional<z.ZodOptional<z.ZodString>>
pushPlatform: z.ZodOptional<z.ZodOptional<z.ZodEnum<{ fcm: 'fcm'; apn: 'apn' }>>>
pushSubscription: z.ZodOptional<
z.ZodOptional<
z.ZodNullable<
z.ZodUnion<
readonly [
z.ZodObject<
{
endpoint: z.ZodOptional<z.ZodString>
keys: z.ZodOptional<
z.ZodObject<{ p256dh: z.ZodString; auth: z.ZodString }, z.core.$strip>
>
},
z.core.$strip
>,
z.ZodObject<
{ registrationId: z.ZodString; registrationType: z.ZodLiteral<'FCM'> },
z.core.$strip
>,
z.ZodObject<{ registrationId: z.ZodString }, z.core.$strip>,
]
>
>
>
>
hasPushSubscription: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>
},
z.core.$strip
>
webPushSubscriptionSchemaWeb Push subscription schema.
const webPushSubscriptionSchema: z.ZodObject<
{
endpoint: z.ZodOptional<z.ZodString>
keys: z.ZodOptional<z.ZodObject<{ p256dh: z.ZodString; auth: z.ZodString }, z.core.$strip>>
},
z.core.$strip
>
authorizersMembers:
authorizers.auth — function: Middleware that checks if the request has an authenticated session (res.locals.session.userId).authorizers.authUser — function: Middleware that verifies the device belongs to the authenticated user. Queries the databasehandlersMembers:
handlers.del — function: Deletes a device by ID from the database.handlers.pushPublicKey — function: Push public key — GET /devices/push/public-key.handlers.query — function: Queries all devices belonging to the authenticated user. Supports pagination via before/afterhandlers.read — function: Reads a single device from res.locals.device, which is set by the authUser authorizerhandlers.update — function: Updates a device's allowed fields: name, pushPlatform, pushSubscription, andtypesMembers:
types.APNPushSubscription — type: APN push subscription type.types.CreateProps — type: Fields required when registering a new device (userId, name, push subscription).types.FCMPushSubscription — type: FCM push subscription type.types.Props — type: Full device record properties (userId, name, push platform/subscription, timestamps).types.PushProps — type: Push notification properties for a device (device ID, platform, subscription data).types.PushSubscription — type: Web Push subscription type.types.UpdateProps — type: Updatable device fields (name, push platform, push subscription).types.Resource — type: An object describing the device resource.types.DeviceService — interface: DeviceService interface for the bond system.zMembers:
z.core — namespacez.infer — typez.output — typez.input — typez.JSONType — typez.globalRegistry — constz.GlobalMeta — interfacez.registry — functionz.config — functionz.$output — constz.$input — constz.$brand — constz.clone — functionz.regexes — namespacez.treeifyError — functionz.prettifyError — functionz.formatError — functionz.flattenError — functionz.TimePrecision — constz.util — namespacez.NEVER — const: A special constant with type neverz.toJSONSchema — functionz.fromJSONSchema — function: Converts a JSON Schema to a Zod schema. This function should be considered semi-experimental. It's behavior is liable to change.z.locales — namespacez.ZodISODateTime — interfacez.ZodISODate — interfacez.ZodISOTime — interfacez.ZodISODuration — interfacez.iso — namespacez.ZodCoercedString — interfacez.ZodCoercedNumber — interfacez.ZodCoercedBigInt — interfacez.ZodCoercedBoolean — interfacez.ZodCoercedDate — interfacez.coerce — namespacez.string — functionz.email — functionz.guid — functionz.uuid — functionz.uuidv4 — functionz.uuidv6 — functionz.uuidv7 — functionz.url — functionz.httpUrl — functionz.emoji — functionz.nanoid — functionz.cuid — function: Validates a CUID v1 string.z.cuid2 — functionz.ulid — functionz.xid — functionz.ksuid — functionz.ipv4 — functionz.mac — functionz.ipv6 — functionz.cidrv4 — functionz.cidrv6 — functionz.base64 — functionz.base64url — functionz.e164 — functionz.jwt — functionz.stringFormat — functionz.hostname — functionz.hex — functionz.hash — functionz.number — functionz.int — functionz.float32 — functionz.float64 — functionz.int32 — functionz.uint32 — functionz.boolean — functionz.bigint — functionz.int64 — functionz.uint64 — functionz.symbol — functionz.any — functionz.unknown — functionz.never — functionz.date — functionz.array — functionz.keyof — functionz.object — functionz.strictObject — functionz.looseObject — functionz.union — functionz.xor — function: Creates an exclusive union (XOR) where exactly one option must match.z.discriminatedUnion — functionz.intersection — functionz.tuple — functionz.record — functionz.partialRecord — functionz.looseRecord — functionz.map — functionz.set — functionz.nativeEnum — functionz.literal — functionz.file — functionz.transform — functionz.optional — functionz.exactOptional — functionz.nullable — functionz.nullish — functionz._default — functionz.prefault — functionz.nonoptional — functionz.success — functionz.nan — functionz.pipe — functionz.codec — functionz.invertCodec — functionz.readonly — functionz.templateLiteral — functionz.lazy — functionz.promise — functionz._function — functionz.check — functionz.custom — functionz.refine — functionz.superRefine — functionz.json — functionz.preprocess — functionz.ZodStandardSchemaWithJSON — typez.ZodType — interfacez._ZodType — interfacez._ZodString — interfacez.ZodString — interfacez.ZodStringFormat — interfacez.ZodEmail — interfacez.ZodGUID — interfacez.ZodUUID — interfacez.ZodURL — interfacez.ZodEmoji — interfacez.ZodNanoID — interfacez.ZodCUID — interfacez.ZodCUID2 — interfacez.ZodULID — interfacez.ZodXID — interfacez.ZodKSUID — interfacez.ZodIPv4 — interfacez.ZodMAC — interfacez.ZodIPv6 — interfacez.ZodCIDRv4 — interfacez.ZodCIDRv6 — interfacez.ZodBase64 — interfacez.ZodBase64URL — interfacez.ZodE164 — interfacez.ZodJWT — interfacez.ZodCustomStringFormat — interfacez._ZodNumber — interfacez.ZodNumber — interfacez.ZodNumberFormat — interfacez.ZodInt — interfacez.ZodFloat32 — interfacez.ZodFloat64 — interfacez.ZodInt32 — interfacez.ZodUInt32 — interfacez._ZodBoolean — interfacez.ZodBoolean — interfacez._ZodBigInt — interfacez.ZodBigInt — interfacez.ZodBigIntFormat — interfacez.ZodSymbol — interfacez.ZodUndefined — interfacez.undefined — functionz.ZodNull — interfacez.null — functionz.ZodAny — interfacez.ZodUnknown — interfacez.ZodNever — interfacez.ZodVoid — interfacez.void — functionz._ZodDate — interfacez.ZodDate — interfacez.ZodArray — interfacez.SafeExtendShape — typez.ZodObject — interfacez.ZodUnion — interfacez.ZodXor — interfacez.ZodDiscriminatedUnion — interfacez.ZodIntersection — interfacez.ZodTuple — interfacez.ZodRecord — interfacez.ZodMap — interfacez.ZodSet — interfacez.ZodEnum — interfacez.enum — functionz.ZodLiteral — interfacez.ZodFile — interfacez.ZodTransform — interfacez.ZodOptional — interfacez.ZodExactOptional — interfacez.ZodNullable — interfacez.ZodDefault — interfacez.ZodPrefault — interfacez.ZodNonOptional — interfacez.ZodSuccess — interfacez.ZodCatch — interfacez.catch — functionz.ZodNaN — interfacez.ZodPipe — interfacez.ZodCodec — interfacez.ZodPreprocess — interfacez.ZodReadonly — interfacez.ZodTemplateLiteral — interfacez.ZodLazy — interfacez.ZodPromise — interfacez.ZodFunction — interfacez.function — functionz.ZodCustom — interfacez.describe — constz.meta — constz.instanceof — functionz.stringbool — constz.ZodJSONSchemaInternals — interfacez.ZodJSONSchema — interfacez.lt — functionz.lte — functionz.gt — functionz.gte — functionz.positive — functionz.negative — functionz.nonpositive — functionz.nonnegative — functionz.multipleOf — functionz.maxSize — functionz.minSize — functionz.size — functionz.maxLength — functionz.minLength — functionz.length — functionz.regex — functionz.lowercase — functionz.uppercase — functionz.includes — functionz.startsWith — functionz.endsWith — functionz.property — functionz.mime — functionz.overwrite — functionz.normalize — functionz.trim — functionz.toLowerCase — functionz.toUpperCase — functionz.slugify — functionz.RefinementCtx — interfacez.ZodIssue — typez.ZodError — interface: An Error-like class used to store Zod validation issues.z.ZodRealError — constz.ZodFlattenedError — typez.ZodFormattedError — typez.ZodErrorMap — interfacez.IssueData — typez.ZodSafeParseResult — typez.ZodSafeParseSuccess — typez.ZodSafeParseError — typez.parse — constz.parseAsync — constz.safeParse — constz.safeParseAsync — constz.encode — constz.decode — constz.encodeAsync — constz.decodeAsync — constz.safeEncode — constz.safeDecode — constz.safeEncodeAsync — constz.safeDecodeAsync — constz.setErrorMap — functionz.getErrorMap — functionz.TypeOf — typez.Infer — typez.ZodFirstPartySchemaTypes — typez.ZodIssueCode — constz.inferFlattenedErrors — typez.inferFormattedError — typez.BRAND — type: Use z.$brand insteadz.ZodTypeAny — interfacez.ZodSchema — interfacez.Schema — interfacez.ZodRawShape — type: Included for Zod 3 compatibilityz.ZodFirstPartyTypeKind — enumThis package exports services that should be registered with the bond system:
import { bond } from '@molecule/api-bond'
import { deviceService } from '@molecule/api-resource-device'
bond('device', deviceService)
Peer dependencies:
@molecule/api-bond ^1.0.1@molecule/api-i18n ^1.0.1@molecule/api-locales-device ^1.0.1@molecule/api-resource ^1.0.1@molecule/api-database ^1.0.1@molecule/api-bond
@molecule/api-database
@molecule/api-i18n
@molecule/api-locales-device
@molecule/api-resource
zod
Table setup: setup/devices.sql ships with this package (the standard
scaffold applies it as a base migration; resource.tableName is devices).
When adding to an existing app, apply it before use.
The handler map is a factory, not a constant. Call
createRequestHandlerMap(createRequestHandler); do not import a
requestHandlerMap constant like other resources export. Its auth /
authUser entries are the authorizer middlewares the routes reference —
authUser enforces that the session user OWNS the device row; removing
either from the map ships the routes ungated (IDOR on other users' devices).
Device rows are created by the auth flow, not by these routes — there is
no POST /devices; signup/login registers the caller's device. Clients then
save their push subscription via
PATCH /devices/:id { pushSubscription, hasPushSubscription } — the
contract the push fan-outs read.
GET /devices/push/public-key is unauthenticated by design (VAPID public
keys are public) and bond-gated: 404 when no push-notifications bond is
wired, 503 when it is wired but unconfigured (VAPID_PUBLIC_KEY unset) —
never a crash. Surface those states in the client instead of retrying.
Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:
name and timestamps — the current session's
device sorted first and flagged isCurrent: true. Rows are created by the
AUTH flow (signup/login → createOrUpdate), NOT a POST route; there is no
POST /devices.name) and the existing row is reused
with a bumped updatedAt — the list count is unchanged, no second row
appears. (The dedup key here is user + name, not a push token.)pushSubscription is what push actually
targets), and a real push fan-out reaches exactly the user's devices where
hasPushSubscription is true (getWithPushSubscription) — a device that
never subscribed receives nothing.pushSubscription and the next push goes to the NEW value,
never the old — one current subscription per device, not a growing list.
Clearing it (hasPushSubscription: false) drops the device from the fan-out
set immediately, so it stops receiving.updatedAt advances on
re-registration / updateLastSeen, so the list reflects recency.deviceId is rejected on its very next request (exists() → false),
for every copy of the token.authUser, which requires the device id AND its userId to
match the session — so guessing another user's device id is rejected (401,
no IDOR into their row or push subscription). The owner is always the session
user: a device is registered under the caller's own userId, and one user
can never PATCH a push subscription onto another user's device.