Valqio Docs
Guides

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:

  1. Select the project and environment your app will call.
  2. Create a product action, such as createWorkspace.
  3. 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.
  4. Attach the backing meter, usage control, limit, feature, or parameter.
  5. 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-node

Use 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.ts

Generated 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 requestId and 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.

On this page