@gatekpr/gatekeeper
v0.1.1
Published
Solana-native API gateway backend with a public Devnet CLI client
Readme
Gatekeeper
Gatekeeper is a Solana-native backend runtime that splits durable control-plane state from real-time execution state.
This refactor keeps the original backend story intact:
- organizations
- roles
- API keys
- policy enforcement
- quota admission
- audit visibility
But it now does so across two execution layers:
- Base Solana for durable control-plane state
- MagicBlock Ephemeral Rollups for delegated real-time runtime state
Project Overview
Gatekeeper models a familiar Web2 API gateway backend in Solana terms:
Organizationis the tenant recordRoleis the RBAC policy objectApiKeyis the credential recordQuotaPolicyis the durable rate-limit policyQuotaRuntimeis the fast mutable execution stateAuditCheckpointis the durable settled summary
The result is a two-layer design:
- base layer stores the canonical configuration and checkpointed runtime state
- delegated runtime handles low-latency request admission and rolling audit history
Architecture Explanation
How this works in Web2
Gatekeeper follows the same split a normal API platform would use in a Web2 stack:
- a database stores tenants, roles, API keys, and quota rules
- a fast mutable store like Redis tracks rolling usage and recent request outcomes
- request middleware checks authorization and quota on the hot path
- a background process compacts recent runtime activity into durable audit state
In Web2 terms, Organization, Role, ApiKey, and QuotaPolicy are control-plane records, while QuotaRuntime behaves like the fast execution-state cache and AuditCheckpoint behaves like the durable settlement record.
How this works on Solana
On Solana, the same backend split is expressed with two execution layers:
- Base Solana stores the durable PDAs:
Gateway,Organization,Role,ApiKey,QuotaPolicy, andAuditCheckpoint - MagicBlock ER handles the high-frequency mutable account:
QuotaRuntime consume_requestis the shared admission path, so the request logic stays the same whether runtime writes happen on base or through delegated executioncommit_quota_runtime,commit_and_undelegate_quota_runtime, andsettle_runtime_checkpointmove fast-path state back into durable base-layer state
This keeps configuration and audit truth on base while moving the hottest mutable path to delegated runtime execution.
Tradeoffs & constraints
- Only
QuotaRuntimeis delegated; this keeps the design legible, but it means settlement is an explicit operator step rather than an invisible background guarantee - Durable audit is checkpointed instead of writing one base account per request; this reduces hot-path cost, but long-term per-request history needs off-chain indexing if full retention is required
- The plain base-RPC path still works without MagicBlock infrastructure; that improves portability, but it does not deliver the same latency profile as delegated execution
- Full ER behavior depends on router-aware transport and compatible infrastructure; local tests focus on the base-compatible path, while ER validation is done with dedicated diagnostics and smoke flows
Devnet Transaction Links
- Program: Gatekeeper on Devnet
- Multi-tenant onboarding: 3J7HCNNm9NeoHSS9Zn2oZfHA8fLy5mQ9fRg28KJeGF13z3x35Ppg1QkFPf82YxKqymBUqQAhRVyNGxgpaTqaDvi
- RBAC allow: 5MoB1k7F3r48ZfSAhLtoQkQhP5fmCQuQgzpkSF7n1GMQpDuLAkFHdiAo4cHpVfJo4To5Tr3GGYtH4UDc5TDMVpHd
- RBAC reject: 4GbHueHxxbvjSUhw2E1M5P7S6Y8FVxc4R5TxBS6cjEuQLsadbLZNhyuNHQFwpAQFmChYrqhPAV1s2XMBnKRvnGGi
- Quota exceeded: 193LyGPZjGsZXjBWxt9UTXQCktFDG3HAgPjZUEgTZqbBcLc1u94S7x23fvZETdJsxxk5ezMwCginbh1nR5WmGUc
- Initial settlement: oDAeJbXxDxgYmBWrsysybFqa1zPdMKbexTAe64hgtD9YeAXDzrPjtB5RMMFutqFvaQw5KaoChAQAbuEW6fscgir
- Revoke key: 48UUPj6FmRHzBEDG2ins7qm3V3KncDVK11XQaHRnJJzREbnhTsR1Q8AmKo2SE65Hrk6sFueoxrK9mofKWwHxvXPP
Why This Matters
A normal Web2 gateway typically splits responsibilities anyway:
- durable config in a database
- hot counters in Redis or memory
- request middleware at the edge
- periodic background compaction or log shipping
Gatekeeper maps that directly onto Solana:
- PDAs on base for durable control-plane truth
- delegated writable runtime accounts for fast admission
- periodic commit and settlement for durability
That makes the architecture readable to backend judges without pretending that every state mutation belongs on the slowest possible path.
Architecture
Base Solana: durable control plane
These accounts remain canonical on base:
GatewayOrganizationRoleApiKeyQuotaPolicyAuditCheckpoint
Base instructions are used for:
- setup
- admin mutations
- durable policy changes
- session finalization
- settled checkpoint updates
MagicBlock ER: execution plane
Fast-path request admission runs against delegated runtime state:
QuotaRuntime
QuotaRuntime contains:
- live window usage
- total allowed/rejected counters
- rolling request sequence
- rolling in-memory-style audit buffer
- delegation metadata
The same consume_request instruction works in both modes:
- plain Solana mode: mutate
QuotaRuntimedirectly on base - ER mode: mutate delegated
QuotaRuntimethrough Magic Router
Durable vs Runtime State
Durable base state
Organization
Seed:
["organization", gateway, name]
Fields:
authorityrole_countapi_key_countsettlement_count
Role
Seed:
["role", organization, role_name]
Fields:
policy_maskcreated_at
ApiKey
Seed:
["api_key", organization, key_id]
Fields:
rolelabelactivecreated_bycreated_atrevoked_at
QuotaPolicy
Seed:
["quota_policy", api_key]
Fields:
max_requestswindow_seconds
This is the durable contract for request admission. It does not store mutable usage.
AuditCheckpoint
Seed:
["audit_checkpoint", api_key]
Fields:
- last settled sequence
- settled allowed/rejected totals
- settled window state
- last settled reason/action
- last settlement timestamp
This is the durable base-layer summary of what the runtime has processed.
Runtime ER state
QuotaRuntime
Seed:
["quota_runtime", api_key]
Fields:
current_window_usedcurrent_window_started_attotal_allowedtotal_rejectedrolling_sequencerolling_auditis_delegateddelegated_validatorcommit_frequency_mslast_settled_sequencelast_settled_at
This is the hot mutable account intended for delegation to MagicBlock.
Request Flow
Plain Solana compatibility path
- Create
QuotaPolicy - Initialize
QuotaRuntime - Call
consume_requeston base - Periodically call
settle_runtime_checkpoint
This keeps the project usable without MagicBlock infrastructure.
ER fast path
- Create
QuotaPolicyon base - Initialize
QuotaRuntimeon base - Delegate
QuotaRuntimewithdelegate_quota_runtime - Route
consume_requestthrough Magic Router - Periodically
commit_quota_runtime - When closing the delegated session, call
commit_and_undelegate_quota_runtime - Finalize durable summary with
settle_runtime_checkpointon base
Instruction Set
Base-layer control plane
initialize_gatewaycreate_organizationcreate_roleattach_policy_to_rolecreate_api_keycreate_quota_policyinitialize_quota_runtimerevoke_api_keysettle_runtime_checkpoint
ER integration hooks
delegate_quota_runtimecommit_quota_runtimecommit_and_undelegate_quota_runtime
Shared request path
consume_request
The request instruction only mutates runtime state. That is the key design change that makes it safe for delegated execution.
Permission Model
Roles use a simple u64 bitmask:
- bit 0:
metrics:read - bit 1:
metrics:write - bit 2:
users:read - bit 3:
users:write - bit 4:
admin
admin is treated as an override bit.
Quota Model
The quota model is intentionally split:
QuotaPolicysays what the rules areQuotaRuntimesays what has happened recently
Admission logic:
- reject if the key is revoked
- reject if the action is invalid
- reject if the role bitmask does not allow the action
- if the current window expired, reset runtime window usage
- reject if
current_window_used >= max_requests - otherwise increment
current_window_used - append a rolling runtime audit record
Audit Model
This refactor no longer writes a new base PDA per request on the hot path.
Instead it uses two audit layers:
Runtime rolling audit
Stored inside QuotaRuntime as a capped rolling buffer.
Each entry records:
- sequence
- action
- allowed/rejected
- reason
- timestamp
- quota usage after the decision
Base settled checkpoint
Stored in AuditCheckpoint.
This is updated by settle_runtime_checkpoint after committed runtime state is safely back on base and no longer delegated.
MagicBlock Integration
Delegation hooks
The program now uses the official MagicBlock Rust SDK:
#[ephemeral]on the program#[delegate]on the runtime delegation accounts#[commit]on commit instructions
This keeps the ER-specific logic isolated to the runtime lifecycle instead of bleeding into the entire control plane.
Why only runtime is delegated
Only QuotaRuntime is on the high-frequency mutable path.
That makes it the right account to delegate. The rest of the model should remain simple base-layer state:
- orgs
- roles
- keys
- durable quota policy
- settled checkpoints
This is the highest-signal architecture for judges because it matches the real separation between config and execution.
Magic Router Client Flow
The CLI now supports two transport modes:
Base RPC
Use normal RPC for:
- setup
- admin
- policy changes
- key management
- runtime initialization
- checkpoint settlement
Magic Router
Use Magic Router for transactions that touch delegated writable accounts:
consume_requestcommit_quota_runtimecommit_and_undelegate_quota_runtime
The CLI supports:
--magic-router <url>- or
MAGIC_ROUTER_URL
Under the hood it uses magic-router-sdk and its account-aware blockhash flow.
Blockhash / routing note
This matters:
- delegated writable transactions must not assume a normal base-layer blockhash is sufficient
- Magic Router derives a blockhash from the writable account set
- the CLI uses
sendMagicTransaction(...)for routed runtime writes
In other words: normal .rpc() is fine for base control-plane instructions, but delegated runtime writes should go through Router-aware transaction sending.
Repository Layout
programs/gatekeeper/ Anchor program with ER hooks
client/gatekeeper.ts Shared TS PDA + router helpers
cli/gatekeeper.ts Base + Magic Router CLI
tests/gatekeeper.ts Plain-Solana runtime compatibility tests
scripts/*.ts Ops + diagnostics for local/devnet runtime flowsLocal Development
Prerequisites
- Rust + Cargo
- Solana CLI
- AVM / Anchor 0.32.1
- Node.js 24+
Setup
avm use 0.32.1
npm install
anchor buildTest Instructions
Rust check
cargo check -p gatekeeperTypeScript check
node_modules/.bin/tsc.cmd --noEmitWeb2 demo test
This scenario-driven test is designed for demos. It walks through:
- tenant onboarding
- RBAC
- API key issuance
- quota enforcement
- audit settlement
- credential revocation
- tenant isolation
npm run test:demoFull local validator flow
avm use 0.32.1
anchor test --skip-buildThe current automated suite validates the plain-Solana compatibility path:
- gateway initialization
- organization creation
- role creation
- API key creation
- quota policy creation
- runtime + checkpoint initialization
- authorized request admission
- unauthorized rejection
- quota exceeded rejection
- window expiry reset
- checkpoint settlement
- invalid-action rejection without quota consumption
- rolling runtime audit cap at eight entries
- revoked-key rejection after settlement
What is not locally automated
Full ER delegation and Router execution require MagicBlock infrastructure. This repo wires the hooks and client flow, but the local automated tests stay focused on the base-compatible path.
Local ER Diagnostics
The repo now includes focused diagnostics for local MagicBlock bring-up.
Router capability probe
This checks whether the endpoint you plan to use for delegated writes actually supports the router-only methods the CLI depends on:
npm run diag:router -- --magic-router http://127.0.0.1:7799
npm run diag:router -- --magic-router http://127.0.0.1:7799 --strict trueThe strict form fails unless the endpoint supports:
getIdentitygetBlockhashForAccountsgetDelegationStatus
Local transport doctor
This verifies:
- base RPC health
- base blockhash fetch
- router capability when
--magic-routeris provided - a plain JavaScript system transfer on base RPC
npm run diag:transport -- --cluster http://127.0.0.1:8899 --wallet wallets/localnet-authority.json
npm run diag:transport -- --cluster http://127.0.0.1:8899 --magic-router http://127.0.0.1:7799 --wallet wallets/localnet-authority.jsonThis is the fastest way to distinguish:
- broken base validator setup
- missing router capabilities
- JavaScript client transport issues
Local runbook
For the full recommended local validation order, see docs/LOCAL_ER_RUNBOOK.md.
Public Devnet Client
The public shared client for this submission is the CLI in this repo. It is designed to work against the deployed Devnet program without requiring a local program build.
Quickstart
Use a funded Devnet wallet JSON file and point the client at Devnet explicitly:
npm install
npm exec --yes --package=@gatekpr/gatekeeper gatekpr help
node ./bin/gatekeeper.mjs help
node ./bin/gatekeeper.mjs init-gateway --cluster https://api.devnet.solana.com --wallet /path/to/devnet-wallet.jsonSelf-contained Devnet demo
This is the fastest public test path for reviewers because it provisions fresh tenant state under their own wallet and prints a JSON report:
npm install
npx tsx scripts/devnet-web2-demo.ts --cluster https://api.devnet.solana.com --wallet /path/to/devnet-wallet.json --run-id reviewer01Public CLI examples
Once a reviewer has created their own org, role, key, and runtime state on Devnet, the shared client can be exercised directly:
node ./bin/gatekeeper.mjs create-org --cluster https://api.devnet.solana.com --wallet /path/to/devnet-wallet.json --name reviewer-acme
node ./bin/gatekeeper.mjs create-role --cluster https://api.devnet.solana.com --wallet /path/to/devnet-wallet.json --org-name reviewer-acme --role-name metrics-reader
node ./bin/gatekeeper.mjs attach-policy --cluster https://api.devnet.solana.com --wallet /path/to/devnet-wallet.json --org-name reviewer-acme --role-name metrics-reader --policy metrics:readCLI Usage
Show help:
node ./bin/gatekeeper.mjs help
npx tsx cli/gatekeeper.ts helpBase setup flow
npx tsx cli/gatekeeper.ts init-gateway
npx tsx cli/gatekeeper.ts create-org --name acme
npx tsx cli/gatekeeper.ts create-role --org-name acme --role-name metrics-reader
npx tsx cli/gatekeeper.ts attach-policy --org-name acme --role-name metrics-reader --policy metrics:read
npx tsx cli/gatekeeper.ts create-key --org-name acme --role-name metrics-reader --label primary --key-material acme-key-1
npx tsx cli/gatekeeper.ts create-quota-policy --org-name acme --key-material acme-key-1 --max-requests 5 --window-seconds 60
npx tsx cli/gatekeeper.ts init-runtime --org-name acme --key-material acme-key-1ER runtime flow
npx tsx cli/gatekeeper.ts delegate-runtime --org-name acme --key-material acme-key-1 --commit-frequency-ms 500 --magic-router https://your-magic-router
npx tsx cli/gatekeeper.ts call-endpoint --org-name acme --key-material acme-key-1 --action metrics:read --magic-router https://your-magic-router
npx tsx cli/gatekeeper.ts commit-runtime --org-name acme --key-material acme-key-1 --magic-router https://your-magic-router
npx tsx cli/gatekeeper.ts undelegate-runtime --org-name acme --key-material acme-key-1 --magic-router https://your-magic-router
npx tsx cli/gatekeeper.ts settle-checkpoint --org-name acme --key-material acme-key-1
npx tsx cli/gatekeeper.ts runtime-status --org-name acme --key-material acme-key-1
npx tsx cli/gatekeeper.ts view-audit --org-name acme --key-material acme-key-1Ops Tooling
The repo now includes production-oriented operator tooling beyond the CLI:
Runtime health snapshot
Returns JSON with:
- delegation state
- rolling vs settled sequence drift
- allowed/rejected totals
- rolling audit entries
- recommended next action
npm run ops:runtime-health -- --org-name acme --key-material acme-key-1Runtime reconciliation
Supports safe operator workflows:
statuscommit-onlysettle-if-safeundelegate-and-settle
npm run ops:reconcile -- --org-name acme --key-material acme-key-1 --mode status
npm run ops:reconcile -- --org-name acme --key-material acme-key-1 --mode settle-if-safe
npm run ops:reconcile -- --org-name acme --key-material acme-key-1 --mode undelegate-and-settle --magic-router https://your-magic-routerDevnet ER smoke flow
This script performs an idempotent control-plane setup and then runs:
- delegate
- routed request admission
- commit
- commit + undelegate
- checkpoint settlement
npm run smoke:devnet:er -- --cluster https://api.devnet.solana.com --magic-router https://your-magic-router --org-name acme --role-name metrics-reader --key-material acme-key-1Failure Recovery Runbook
Case: runtime is delegated and traffic has accumulated
Expected operator path:
runtime-statuscommit-runtimeif you want to keep the session openundelegate-runtimewhen closing the delegated windowsettle-checkpointon base
Case: runtime is not delegated but drift is non-zero
That means runtime state has advanced beyond the durable checkpoint.
Use:
npm run ops:reconcile -- --org-name acme --key-material acme-key-1 --mode settle-if-safeCase: runtime status is unclear
Use:
npm run ops:runtime-health -- --org-name acme --key-material acme-key-1The script prints a recommended action based on:
- delegated vs undelegated state
- rolling sequence drift
- rolling audit occupancy
Production Hardening Gaps
This repo is stronger operationally now, but it is still not production-ready.
Remaining gaps:
- live MagicBlock ER integration testing on persistent infrastructure
- security review / formal audit
- alerting and dashboards around missed commits or checkpoint lag
- multi-operator auth and key management policies
- archival/indexing for long-term audit retention
- throughput and cost benchmarking under real traffic
- incident playbooks for router outages and validator-specific failures
Design Tradeoffs
- Only the hot runtime account is delegated.
consume_requestmutates runtime only.- Durable audit is checkpointed, not expanded into one base PDA per request.
- Full local ER execution is not mocked in tests.
- The checkpoint finalization step is intentionally explicit so the architecture stays inspectable.
Why This Is A Better Judge Story
This version tells a clearer Solana-native backend story than a single-layer contract:
- base Solana is the durable control plane
- MagicBlock ER is the execution plane
- Router-aware clients send delegated writes correctly
- settlement is explicit
- the plain-Solana path still works without ER
That is a stronger backend-system translation than pretending every request mutation belongs in the same durability tier.
