> For the complete documentation index, see [llms.txt](https://govplane.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://govplane.gitbook.io/docs/documentation/sdk-for-javascript-node.js/installation-and-quick-start.md).

# Installation & Quick Start

`@govplane/runtime-sdk` evaluates Govplane policy bundles inside your Node.js process — from a file on disk, or from Govplane Cloud.

```bash
npm install @govplane/runtime-sdk
```

**Zero runtime dependencies.** Node.js 20.19 or later.

### Current Version: 2.1.0

### Requirements

|              |                                               |
| ------------ | --------------------------------------------- |
| Node.js      | **20.19** or later (or 22.12+ on the 22 line) |
| TypeScript   | 5.0+ — 5.8+ if you compile to CommonJS        |
| Dependencies | none                                          |

The 20.19 floor is not arbitrary: it is the first release where `require()` of an ES module works, which is what lets one build serve both module systems.

### ESM and CommonJS

The package ships **one** build, in ESM. Both module systems load that same file:

```js
import { createLocalClient } from '@govplane/runtime-sdk';        // ESM
const { createLocalClient } = require('@govplane/runtime-sdk');   // CommonJS
```

{% hint style="info" %}
There is no separate CommonJS build **on purpose**. A package that ships both can be loaded twice in one process, and then its classes are no longer shared — an error thrown by one copy fails an `instanceof` check against the other. The SDK's error classes are meant to be checked, so it ships one file and that cannot happen.
{% endhint %}

On Node older than 20.19, `require()` fails with `ERR_REQUIRE_ESM`. Use a dynamic import:

```js
async function start() {
  const { createLocalClient } = await import('@govplane/runtime-sdk');
  return createLocalClient({ bundlePath: './policy-bundle.json', allowUnsigned: true });
}
```

TypeScript compiling to CommonJS needs TypeScript 5.8+ with `"module": "nodenext"` for `require()` of an ES module to typecheck.

### Getting a bundle

The SDK evaluates bundles; it does not author them. Build one with the CLI Toolkit:

```bash
npm install --global @govplane/cli @govplane/cli-toolkit
govplane analyze --source .     # find where policy is evaluated
govplane policies add-policy --policy-key login-protection --defaults-effect allow
govplane build --signed         # → policy-bundle.json
```

See Policy Drafts for the authoring format.

### Quick start — local

No account, no credentials, no network.

```ts
import { createLocalClient } from '@govplane/runtime-sdk';

const govplane = await createLocalClient({
  bundlePath: './policy-bundle.json',
  verify: { algorithm: 'ECDSA_SHA_256', publicKey: process.env.GOVPLANE_PUBLIC_KEY },
});

const decision = govplane.evaluate({
  target:  { service: 'auth', resource: 'login', action: 'authenticate' },
  context: { failedAttempts: 6 },
});

if (decision.decision === 'deny') {
  return reply.status(403).send({ error: 'Forbidden' });
}
```

Two things to notice:

* **The factory is `async` and you `await` it.** The bundle is read and verified *before* the client exists, so `evaluate()` can never be called against nothing. There is no `start()` and no "not ready yet" state.
* **`verify` is required** unless you pass `allowUnsigned: true`. A bundle decides who may do what; running one whose provenance you cannot establish should be a decision somebody made on purpose.

To try it before you have signing set up:

```ts
const govplane = await createLocalClient({
  bundlePath: './policy-bundle.json',
  allowUnsigned: true,          // development only
});
```

### Quick start — Govplane Cloud

```ts
import { createRemoteClient } from '@govplane/runtime-sdk';

const govplane = await createRemoteClient({
  baseUrl:    process.env.GOVPLANE_URL,
  runtimeKey: process.env.GOVPLANE_RUNTIME_KEY,
  orgId:      'org_…',
  projectId:  'proj_…',
  env:        'prod',
  verify:     { algorithm: 'ECDSA_SHA_256', publicKey: process.env.GOVPLANE_PUBLIC_KEY },
  poll:       { intervalMs: 5000 },
});
```

Same client, same `evaluate`, same decisions. What is added is delivery — polling, ETag revalidation, backoff and degraded-mode reporting.

### The client

Both factories return the same interface:

```ts
govplane.evaluate({ target, context });                       // → Decision
govplane.evaluate({ ruleId, context });                       // one named rule
govplane.evaluate({ target, context }, { asBoolean: true });  // → boolean
govplane.evaluateWithTrace({ target, context }, { level: 'full' });

govplane.bundle();            // the bundle currently in force
govplane.status();            // origin, version, checksum, signature, policy count
await govplane.reload();      // re-read the source now
govplane.close();             // stop watching or polling
```

```ts
govplane.status();
// {
//   origin: 'local',
//   location: '/srv/app/policy-bundle.json',
//   bundleVersion: 3,
//   checksum: 'sha256:9f2c…',
//   env: 'prod',
//   signature: { status: 'verified', algorithm: 'ECDSA_SHA_256', keyId: 'release-2026-08' },
//   policies: 4,
//   loadedAt: '2026-08-08T09:00:00.000Z'
// }
```

`status()` is what to log at boot and expose on a health endpoint. `signature.status` is `verified` or `unsigned`, and it is the field worth alerting on.

### Using the engine directly

If you already hold a bundle — loaded from a database, injected by configuration, or constructed in a test — you can skip the client and use the engine:

```ts
import { createPolicyEngine } from '@govplane/runtime-sdk';
import { readFileSync } from 'node:fs';

const bundle = JSON.parse(readFileSync('bundle.json', 'utf8'));

const engine = createPolicyEngine({
  getBundle: () => bundle,
  validateContext: false,       // see the warning below
  parseCustomEffect: true,
});

const decision = engine.evaluate({ target, context });
```

{% hint style="warning" %}
`createPolicyEngine` performs **no verification**. It evaluates whatever you hand it. The client factories exist to make verification the default path — reach for the engine when you have already established provenance yourself, or when there is none to establish (a test fixture).
{% endhint %}

{% hint style="danger" %}
**The engine validates context by default; the clients do not.** Left alone, `createPolicyEngine` enforces a seven-key *sample* policy — `plan`, `country`, `requestTier`, `feature`, `amount`, `isAuthenticated`, `role` — and every other key throws:

```
Error: Context key not allowed: failedAttempts
```

Pass `validateContext: false`, or pin your own `contextPolicy`. `createLocalClient` and `createRemoteClient` already do this for you, which is why the examples above need no such flag.
{% endhint %}

`getBundle` is a function rather than a value so the engine always reads the current bundle — that is how the client swaps in a reloaded one without rebuilding anything.

The engine also exposes `flushTraces()`, which the client does not — see Traces.

### Framework example

```ts
// Express middleware
const requirePolicy = (target: Target) => (req, res, next) => {
  const decision = govplane.evaluate({
    target,
    context: { plan: req.user.plan, isAuthenticated: Boolean(req.user) },
  });

  switch (decision.decision) {
    case 'allow':
      return next();
    case 'throttle':
      res.setHeader('Retry-After', String(decision.throttle.windowSeconds));
      return res.status(429).json({ error: 'Too Many Requests' });
    case 'kill_switch':
      return res.status(503).json({ error: decision.killSwitch.reason ?? 'Unavailable' });
    default:
      return res.status(403).json({ error: 'Forbidden', policy: decision.policyKey });
  }
};

app.post('/invoices', requirePolicy({ service: 'api', resource: 'invoices', action: 'create' }), handler);
```

### Next

* [Configuring the SDK](/docs/documentation/sdk-for-javascript-node.js/configuring-the-js-sdk.md) — every option, and the defaults worth knowing
* [Evaluating Decisions](/docs/documentation/sdk-for-javascript-node.js/evaluating-decisions.md) — the `evaluate` API in full
* [Best Practices](/docs/documentation/sdk-for-javascript-node.js/best-practices.md) — how to run this in production
* [Errors](/docs/documentation/sdk-for-javascript-node.js/errors.md) — every error code and what to do about it
* [Migrating from 1.x](/docs/documentation/sdk-for-javascript-node.js/migrating-from-1.x.md) — if you are upgrading


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://govplane.gitbook.io/docs/documentation/sdk-for-javascript-node.js/installation-and-quick-start.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
