An on-chain API key management system built with Anchor — demonstrating how Web2 backend patterns map to Solana on-chain programs.
Built for: Solana Hackathon — "Rebuild Backend Systems as On-Chain Rust Programs"
Network: Devnet
Program ID: 9uvbWGNcfJ74Jv2KZVE56NWeXKb7P4CUzSPwQ7PWQGiB
This system replaces a traditional backend API key service (think: Stripe, Sendgrid, or any SaaS) with a fully on-chain program. No database, no server, no middleware. Just accounts and instructions.
Authority (owner)
│
└── Registry PDA ["registry", authority]
│
├── ApiKey PDA ["api_key", registry, 0] ← key #0
├── ApiKey PDA ["api_key", registry, 1] ← key #1
└── ApiKey PDA ["api_key", registry, N] ← key #N
Key design choices:
- The raw API key is never stored on-chain — only its SHA-256 hash
- PDAs serve as deterministic, ownerless database rows
- Authorization is enforced by account constraints, not application code
- Expiry is checked against the Solana
Clocksysvar (no trusted timestamp needed)
In a traditional Web2 system, API key management looks like:
PostgreSQL database:
┌─────────────────────────────────────────────────────────┐
│ Table: api_registries │
│ id (uuid PK), name, owner_user_id, key_count │
├─────────────────────────────────────────────────────────┤
│ Table: api_keys │
│ id (uuid PK), registry_id (FK), key_hash, owner, │
│ permissions, expires_at, is_active, created_at │
└─────────────────────────────────────────────────────────┘
REST API:
POST /registries → create registry (auth: JWT)
POST /registries/:id/keys → issue key (auth: JWT)
DELETE /keys/:id → revoke key (auth: JWT)
GET /keys/:id/verify → verify key (public)
Auth middleware:
- JWT bearer token check on every request
- Registry ownership check before issue/revoke
- Hash comparison during verify
Every Web2 concept maps directly to a Solana primitive:
| Web2 concept | Solana equivalent | Details |
|---|---|---|
| Database table | Account type (struct) | Registry, ApiKey structs in Rust |
| Table row | PDA | Deterministic address derived from seeds |
| Primary key | PDA seeds | ["registry", authority], ["api_key", registry, key_id] |
| Foreign key | Stored Pubkey field |
ApiKey.registry: Pubkey |
| CRUD API | Instructions | create_registry, issue_key, revoke_key, verify_key |
| Auth middleware | Account constraints | has_one = authority @ Unauthorized |
| System clock | Clock sysvar |
Clock::get()?.unix_timestamp |
| Transaction log | On-chain event | emit!(VerifyEvent { ... }) |
| App server | Solana validators | Distributed, always-on execution |
create_registry(name: String)
Signers: authority
Accounts: registry (init PDA), authority (mut), system_program
Logic: Init Registry PDA with authority, name, key_count=0
issue_key(key_hash: [u8;32], permissions: u64, expires_at: i64)
Signers: authority
Accounts: registry (mut), api_key (init PDA), authority (mut), owner, system_program
Logic: Init ApiKey PDA, increment registry.key_count
revoke_key()
Signers: authority
Accounts: registry, api_key (mut), authority
Logic: Set api_key.is_active = false; verify authority owns registry
verify_key(key_hash: [u8;32])
Signers: any (or no signer needed if using RPC read)
Accounts: api_key
Logic: Check is_active, check expiry vs Clock, check hash match, emit VerifyEvent
| Bit | Value | Meaning |
|---|---|---|
| 0 | 1 | READ |
| 1 | 2 | WRITE |
| 2 | 4 | ADMIN |
Combine: permissions = 3 means READ + WRITE. permissions = 7 means all.
Advantages over Web2:
- No server downtime — validators are always running
- Transparent — all key states are publicly verifiable
- No vendor lock-in — anyone can build a client
- Immutable audit trail — revocations are on-chain forever
Constraints:
- Storage cost: Each account requires rent (lamports locked). A Registry costs ~0.001 SOL; each ApiKey ~0.001 SOL.
- Compute: Every instruction costs compute units.
verify_keyis free to read off-chain but costs CU if called as a transaction. - No secret storage: The raw key cannot be recovered on-chain by design. If you lose it, you must revoke and reissue.
- No delete: Accounts can be closed (rent reclaimed) but this wasn't implemented in v1. Revoke is permanent.
- Clock precision:
Clock.unix_timestampis block time, not wall clock. ~400ms slot time means precision is sufficient for key expiry.
create_registry: 3EV2s2nFTqZrZHSisWxt9WoTR8s54nSQnKAexuDGuLMn1CZidxpgFgwTK7fFG3MReZ7z8DC2MRPP7txHAZKDS3wWissue_key: 29wPg4ChxiS3S9irwviG4PFZyJdWbX6ExDkJgvXdtABCx2a4pjVcRrKp63fBaK9BESPgyrHeJ4uLpNkFWLZQMStYverify_key: verified via CLI (read-only, no tx signature emitted by default)revoke_key: 2esAhNLeBPyT3wRzXHvZqpDw8wBUztjJHiRbAJQomDktnqZeHfqu1pk25vAqm8MVhqC4yDSRoESWUdjQAAuwUGYE
Program on devnet: 9uvbWGNcfJ74Jv2KZVE56NWeXKb7P4CUzSPwQ7PWQGiB
# Solana CLI
sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"
# Anchor CLI (macOS)
brew install anchor
# Node.js dependencies
yarn installsolana-keygen new -o ~/.config/solana/id.json
solana airdrop 2 <YOUR_PUBKEY> --url devnetcd apps/solana-api-key-manager
# Build the program
anchor build
# Deploy to devnet (requires ~2 SOL for deployment)
anchor deploy --provider.cluster devnetAfter deploy, update the program ID in Anchor.toml and cli/src/index.ts.
yarn install
anchor test --provider.cluster devnet# Create a registry
npx ts-node cli/src/index.ts create-registry \
--name "MyService" \
--keypair ~/.config/solana/id.json \
--program-id 9uvbWGNcfJ74Jv2KZVE56NWeXKb7P4CUzSPwQ7PWQGiB
# Issue a new API key (generates and prints the raw key — SAVE IT)
npx ts-node cli/src/index.ts issue-key \
--registry <REGISTRY_PDA> \
--owner <OWNER_PUBKEY> \
--permissions 3 \
--program-id 9uvbWGNcfJ74Jv2KZVE56NWeXKb7P4CUzSPwQ7PWQGiB
# Issue a key with expiry
npx ts-node cli/src/index.ts issue-key \
--registry <REGISTRY_PDA> \
--owner <OWNER_PUBKEY> \
--permissions 1 \
--expires 2027-01-01 \
--program-id 9uvbWGNcfJ74Jv2KZVE56NWeXKb7P4CUzSPwQ7PWQGiB
# Verify a key (checks hash + active + not expired)
npx ts-node cli/src/index.ts verify-key \
--registry <REGISTRY_PDA> \
--key-id 0 \
--raw-key <RAW_KEY_FROM_ISSUE_STEP> \
--program-id 9uvbWGNcfJ74Jv2KZVE56NWeXKb7P4CUzSPwQ7PWQGiB
# Revoke a key
npx ts-node cli/src/index.ts revoke-key \
--registry <REGISTRY_PDA> \
--key-id 0 \
--program-id 9uvbWGNcfJ74Jv2KZVE56NWeXKb7P4CUzSPwQ7PWQGiBsolana-api-key-manager/
programs/
api-key-manager/
src/
lib.rs # Program entrypoint + declare_id!
instructions/
create_registry.rs
issue_key.rs
revoke_key.rs
verify_key.rs
state/
registry.rs # Registry account (PDA)
api_key.rs # ApiKey account (PDA)
errors.rs # Custom error enum
Cargo.toml
tests/
api-key-manager.ts # Anchor Mocha integration tests (7 test cases)
cli/
src/
index.ts # Commander CLI entrypoint
commands/
create-registry.ts
issue-key.ts
revoke-key.ts
verify-key.ts
utils/
crypto.ts # SHA-256 hashing
keypair.ts # Load keypair from file
Anchor.toml
package.json
tsconfig.json