End-to-end generated client
Configure Console, publish runtime state, generate a client, call Valqio from app code, and inspect the evidence.
This guide shows the full public integration loop for one server-side product action. Use it when your project has product actions in Console and you want app-native helpers instead of raw meter keys in product code.
The path is deliberately narrow: publish one environment, generate one client, call Valqio before one billable action, then inspect evidence.
1. Model the product action
In Console:
- Select the project and environment your app will call.
- Create a product action, such as
createWorkspace. - Choose the backing operation:
- Check and count usage for immediate work.
- Reserve usage first for expensive or asynchronous work.
- Check access for feature or parameter access.
- Acquire limit for concurrent work.
- Attach the backing meter, usage control, limit, feature, or parameter.
- Attach the action to the relevant plan or customer policy.
The action should name product work, not infrastructure. Good names are createWorkspace, renderVideo, callApi, sendInvoice, and generateText.
2. Publish the environment
Open Publish in Console and preview the runtime state. Confirm the product action, meter, limit, plan, and customer policy counts look right.
Then publish the environment. Publishing is what lets the hosted data plane and generated client serve decisions from the same environment state.
3. Create a runtime key
Open Developers in Console and create a server runtime key for the same environment.
Store these values as backend secrets:
VALQIO_API_KEY=<server runtime key>
VALQIO_DATA_PLANE_URL=<hosted runtime API URL>
VALQIO_PROJECT_ID=<project id>
VALQIO_ENVIRONMENT=<environment key>Do not expose server runtime keys in browser code.
4. Install SDK tooling
Install the Node SDK in your server app:
npm install @valqio/sdk-nodeUse the generated-client command or file shown in Console. When CLI access is enabled for your account, the command will look like this:
npx @valqio/cli schema generate \
--from applied \
--project <project-id> \
--env production \
--out app/server/valqio.generated.tsGenerated client files come from Console or Valqio's SDK tooling for your project. Use the exact Console command for your project and sign in with your Valqio account if the CLI asks for authentication.
5. Configure the generated client
import { createMyAppValqioClientFromEnv, inspectMyAppValqioReadiness } from './valqio.generated'
async function createReadyMyAppClient() {
const bindings = inspectMyAppValqioReadiness()
if (!bindings.ready) {
throw new Error(`Valqio bindings unresolved: ${bindings.unresolved.join(', ')}`)
}
const myApp = createMyAppValqioClientFromEnv({
runtimeOverrides: {
mode: 'remote-only',
},
})
const runtime = await myApp.ready()
if (!runtime.snapshotReady) {
throw new Error('Valqio published runtime snapshot is unavailable')
}
return myApp
}
export const myAppPromise = createReadyMyAppClient()The synchronous inspector validates generated bindings. The asynchronous ready() call checks whether the runtime client can obtain a published snapshot. After the first load, it may use the client's cached snapshot, so treat it as snapshot readiness rather than a general data-plane health probe. Fail fast on either setup check instead of running billable product work without enforcement.
6. Call Valqio before work runs
For work that should reserve value before it starts:
The recordPendingValqio* and markValqio*Applied calls below represent app-owned durable storage. A reconciliation worker should retry any pending operation with the same idempotency key.
const requestId = 'req_runtime_01'
const customerId = 'workspace_acme'
const idempotencyBase = `createWorkspace:${requestId}`
export async function handleCreateWorkspace() {
const myApp = await myAppPromise
const customer = myApp.customer({ id: customerId }, { requestId })
const keys = customer.actions.createWorkspace.createIdempotencyKeys(idempotencyBase)
const reserved = await customer.actions.createWorkspace.reserve({
quantity: 1,
requestId,
idempotencyKey: keys.reserve,
context: {
endpoint: '/workspaces',
},
})
if (!reserved.allowed || !reserved.reservation?.reservationId) {
return {
status: 402,
body: {
reasonCode: reserved.reasonCode,
requestId,
},
}
}
const reservationId = reserved.reservation.reservationId
let workspace
try {
workspace = await createWorkspace()
} catch (error) {
await recordPendingValqioRelease({
reservationId,
requestId,
idempotencyKey: keys.release,
})
await customer.actions.createWorkspace.releaseWithRetry({
reservationId,
requestId,
idempotencyKey: keys.release,
metadata: { reason: 'work_failed' },
})
await markValqioReleaseApplied(reservationId)
throw error
}
await recordPendingValqioCommit({ reservationId, requestId, idempotencyKey: keys.commit })
await customer.actions.createWorkspace.commitWithRetry({
reservationId,
requestId,
idempotencyKey: keys.commit,
})
await markValqioCommitApplied(reservationId)
return { status: 200, body: { workspace, requestId } }
}If commit remains uncertain after retries, leave the pending-commit record in place and reconcile with the same commit key. Never release after product work has succeeded.
Generated helper names depend on the product action operation. Use enforce for check-and-count usage actions, reserve or withReservation for reservation actions, and run when you want to execute the generated action definition directly.
For immediate work, choose one generated action or its underlying meter enforce helper; do not call both for the same unit. This example uses the action and keeps the same request identity pattern:
export async function handleSendInvoice(customerId: string, requestId: string) {
const myApp = await myAppPromise
const customer = myApp.customer({ id: customerId }, { requestId })
const action = await customer.actions.sendInvoice.run({
requestId,
idempotencyKey: `sendInvoice:${requestId}`,
})
if (!action.ok) {
return {
status: action.status === 'blocked' ? 402 : 503,
body: { status: action.status, requestId },
}
}
const invoice = await sendInvoice()
return { status: 200, body: { invoice, requestId } }
}7. Inspect decision evidence
Open Decisions → History in Console and search by request ID, subject, product action, meter, or outcome.
Confirm:
- the subject is the expected customer or workspace
- the action or meter matches the generated helper
- the decision happened before product work ran
- the idempotency key is stable for retries
- the deny reason is product-specific enough for your app response
- the reservation ID is present for reservation flows
8. Trace Runtime Activity
Open Runtime Activity and check the lifecycle:
- reserve created before work started
- commit recorded after successful work
- release recorded after failed or cancelled work
- repeated commit or release calls reused the same idempotency key
- support trace identifiers match app logs
Runtime Activity is the proof that the usage transaction lifecycle settled correctly.
9. Review billing sync
If billing sync is enabled, open Draft Invoices for the customer and period.
Review:
- usage source context
- recent usage by meter
- exportable lines
- blocked or failed lines
- selected Stripe connection
- Provider IDs returned by Stripe and export evidence after export
Billing providers invoice and collect payment. Valqio records the runtime evidence and usage outcomes that explain what should be synced.
Production checklist
- The app uses the hosted data plane URL from Console.
- The runtime key belongs to the same project and environment as the generated client.
- The generated client was refreshed after publishing product action changes.
- Every billable request sends
requestIdand a stable idempotency key. - The app handles deny without running product work.
- Reservation flows commit after success and release after failure.
- Support can find evidence from request ID without seeing secrets.