Sunscreen
A Rust CLI for scaffolding, repairing, and orchestrating Solana Anchor and Pinocchio workspaces.
Sunscreen takes you from an empty folder to a working Solana program — Anchor or Pinocchio — without hand-stitching anchor, solana, cargo, codama, surfpool, and frontend tooling. It is deterministic, marker-driven, and built for the long term: incremental edits, plugin extension, supervised dev loop.
Pick your path
🛠 Guides
Task-oriented walkthroughs. "How do I scaffold a CRUD?" "How do I deploy to devnet?"
📖 Reference
Every command, every flag, every exit code. For when you know what you want.
Why sunscreen?
- Deterministic generation. Same inputs, same files, byte-for-byte. Golden-tested.
- Marker-based incremental edits. Add a new instruction to an existing program without breaking what's there.
chain doctor --fix-markersrepairs safe drift. - Supervised dev loop.
chain serveruns Surfpool (orsolana-test-validator), watches files, rebuilds, regenerates Codama clients, notifies the frontend — all in one process. - Plugins. Local plugins extend
scaffold <noun>via stdio JSON-RPC; gRPC contract for the future. Trust and sandboxing are explicit. - Two frameworks. Anchor for the productive default; Pinocchio for the bare-metal path.
Status
v0.1.0 is the first preview release. The path to v1.0 is tracked in the Roadmap. Phase 8 (this site, completions, Homebrew/Windows distribution, release polish) is in progress.
What is sunscreen?
⏱ 5 min read · 🎯 you'll understand: what sunscreen does, when to use it, when not to.
Sunscreen is a command-line tool for Solana developers. It scaffolds programs, manages a local dev loop, generates clients, and is extensible by plugins.
If you've ever written a Solana program, you know the friction: cargo, anchor, solana-cli, IDL generation, a frontend, a local validator, hot reload — five tools, five mental models. Sunscreen is one CLI that orchestrates them, with one config file (sunscreen.yml).
What sunscreen does
- Creates a new Anchor or Pinocchio workspace from a single command.
- Adds instructions, accounts, events, errors, or full recipes (CRUD, SPL Token, Metaplex NFT) to an existing program — without breaking what's there. It uses markers in your code to edit the same files safely on every run.
- Builds and watches in one supervised process:
chain serveruns Surfpool (orsolana-test-validator), watches files, rebuilds with Anchor, regenerates Codama clients, and tells your frontend to reload. - Generates clients: IDL, JavaScript via Codama, React/Solid Query hooks — all deterministic, all idempotent.
- Onboards beginners:
init,quickstart token|nft|dao|blog, embedded examples, wallet helpers, deploy plans, and actionable errors that tell you the next step.
When to use sunscreen
- You're starting a new Solana program and want a clean workspace in 30 seconds.
- You want a single tool that scaffolds, builds, runs a local validator, and regenerates clients on file changes.
- You like declarative configs (
sunscreen.yml) over running 6 separate commands. - You want the option to extend the CLI with plugins for your team.
When not to use sunscreen
- Your program is already deeply customized in a non-Anchor, non-Pinocchio workflow. Sunscreen targets these two frameworks.
- You need Windows distribution today — that's planned for v1.0 (see Roadmap). macOS and Linux work now.
- You want to manage validator clusters in production — sunscreen targets local dev. Use Helius or similar for prod RPC.
How it compares to Anchor CLI alone
Anchor CLI gives you anchor init, anchor build, anchor deploy. Great primitives. Sunscreen sits one level above:
| Task | Anchor CLI | Sunscreen |
|---|---|---|
| Create workspace | anchor init | sunscreen chain new --framework anchor --frontend vite |
| Add instruction | hand-edit lib.rs | sunscreen scaffold instruction Create --program app |
| Add CRUD slice | manual (~200 lines) | sunscreen scaffold crud Post --program app |
| Watch + rebuild + regenerate clients | run 3 terminals | sunscreen chain serve |
| Diagnose toolchain | trial and error | sunscreen doctor |
Anchor stays under the hood. Sunscreen does not replace it; it composes with it.
Next
- Install it.
- Make your first workspace.
- Or jump straight to your first NFT in 10 minutes.
Installing
⏱ 3 min · 🎯 you'll have: sunscreen --version working in your terminal.
Sunscreen ships as a single binary. Three install paths, in order of recommendation.
Pre-requisites
You need a recent stable Rust toolchain on your PATH. If you don't have it:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
The other Solana tools (anchor, solana, cargo-build-sbf, pnpm, …) are checked at runtime by sunscreen doctor. You only need them when you actually run the corresponding command.
Option 1 — installer script (recommended)
The fastest path. Works on macOS (x86_64, aarch64) and Linux (x86_64, aarch64).
curl --proto '=https' --tlsv1.2 -LsSf \
https://github.com/Pantani/sunscreen/releases/download/v0.1.0/sunscreen-installer.sh \
| sh
The script installs to ~/.cargo/bin/sunscreen. Restart your shell or source ~/.cargo/env so the binary is on PATH.
Option 2 — cargo install
If you already have a Rust toolchain and prefer to build locally:
cargo install --git https://github.com/Pantani/sunscreen --tag v0.1.0 sunscreen
This compiles from source (~2 minutes on a modern laptop) and places the binary in ~/.cargo/bin.
Option 3 — download the archive
Releases include pre-built tarballs and zips:
- Open https://github.com/Pantani/sunscreen/releases/latest.
- Download the archive matching your platform (
sunscreen-aarch64-apple-darwin.tar.xz,sunscreen-x86_64-unknown-linux-gnu.tar.xz, …). - Extract and move
sunscreeninto a directory on your$PATH.
Verify
sunscreen --version
# sunscreen 0.1.0
Run the doctor to see which Solana tools sunscreen detected:
sunscreen doctor
You'll see a table. Anything reported missing will be required only when you run a command that needs it. For example, you can chain new without anchor installed, but you'll need anchor to chain build.
Updating
- Installer script: re-run with the new version URL.
cargo install: re-run with--forceand the new--tag.- Archive: download a new tarball.
Uninstall
rm "$(which sunscreen)"
Sunscreen does not write to global directories outside the binary itself. Per-workspace state lives in your project folder.
Next
Your first workspace
⏱ 8 min · 🎯 you'll have: a freshly scaffolded Anchor workspace, building locally.
We'll create an empty workspace, look at what was generated, and run our first build.
Pre-requisites
sunscreeninstalled (Installing).anchorCLI on your PATH (anchor --versionshould print a version). If missing, install via AVM.cargo(comes with Rust).
Don't have everything? Run sunscreen doctor first — it tells you exactly what's missing.
Step 1 — Create
sunscreen chain new my-app --framework anchor --frontend none
You'll see progress output, then a summary similar to:
✓ workspace my-app/ created (anchor, no frontend)
--frontend accepts none, vite, or next. Pick vite if you want React + Vite scaffolded under app/; pick next for a Next.js scaffold. none keeps the workspace lean.
cd my-app
tree -L 2 -I node_modules
my-app/
├── Anchor.toml
├── Cargo.toml
├── package.json
├── programs/
│ └── my_app/
│ ├── Cargo.toml
│ └── src/
│ └── lib.rs
├── sunscreen.yml ← sunscreen's config (the source of truth)
├── tests/
│ └── my_app.ts
└── target/
The interesting files:
sunscreen.yml— your project config. Programs, plugins, runtime settings.programs/my_app/src/lib.rs— the Anchor program. Has// sunscreen:begin ... endmarkers that sunscreen can edit on later commands without breaking what you wrote in between.Anchor.toml— standard Anchor config, points at the program.
The markers in lib.rs are how sunscreen does incremental edits. When you run sunscreen scaffold instruction Foo later, it injects code only into marked regions. Your hand-written code is left alone. Read more in Incremental scaffolding.
sunscreen chain build
This runs anchor build under the hood and reports progress as NDJSON lines (one event per line, useful for editor integrations). On success:
✓ anchor build
✓ codama clients regenerated (skipped: no frontend)
Behind the scenes:
anchor buildproducedtarget/idl/my_app.jsonand the program's.so.- Sunscreen skipped Codama client generation because we picked
--frontend none.
Step 4 — Add an instruction
sunscreen scaffold instruction Greet --program my_app
Open programs/my_app/src/lib.rs — you'll see a new pub fn greet(...) handler inside the program module, registered in the dispatch. The marker regions made the edit safe.
Re-run the same command:
sunscreen scaffold instruction Greet --program my_app
# error: instruction "Greet" already exists in program "my_app" (exit code 4)
Idempotent: sunscreen refuses to clobber. Use --force if you really want to regenerate.
What just happened
- One command (
chain new) gave you a buildable Anchor workspace plus sunscreen's own config. - The generated code has marker comments that let sunscreen safely edit the same files in later runs.
chain buildis a thin orchestration overanchor build+ Codama generation.scaffold instructionmutated marked regions only, leaving your code untouched.
Next
- Your first NFT in 10 minutes — chain
init+ scaffold + deploy. - Scaffolding a CRUD resource — composite recipes.
- Incremental scaffolding — the marker model in depth.
Your first NFT in 10 minutes
⏱ 10 min · 🎯 you'll have: a Metaplex NFT scaffold ready to mint on devnet.
We'll go from empty folder to a mintable NFT program using sunscreen's quickstart recipe.
Pre-requisites
sunscreen(Installing).anchorCLI,solanaCLI, Node 18+ withpnpm.- A Solana keypair (we'll create one if you don't have it).
Run sunscreen doctor to confirm. Anything missing will be flagged.
Step 1 — Quickstart
sunscreen quickstart nft my-first-nft
cd my-first-nft
This single command:
- Created an Anchor workspace with a Vite frontend.
- Scaffolded a Metaplex NFT recipe slice (mint, metadata, master edition).
- Added a sample frontend hook for minting (TanStack Query).
You'll see a summary table at the end listing files created.
quickstart is a composition of more granular commands: chain new, scaffold metaplex-nft, generate frontend-hooks. You can run them individually for more control — quickstart is the beginner-friendly shortcut.
sunscreen chain build
Successful build produces target/idl/my_first_nft.json and regenerates Codama clients into app/src/clients/ (auto-wired because we picked the React frontend).
Step 3 — Configure a devnet wallet
If you don't have a Solana keypair:
sunscreen wallet new dev
sunscreen wallet set-default dev --cluster devnet
The first command creates a keypair under .sunscreen/wallets/dev.json and prints the public key. The second makes it sunscreen's default for devnet operations.
Get devnet SOL:
sunscreen wallet airdrop 2 --cluster devnet
If the airdrop is throttled (common), the error tells you the next step (use solana airdrop directly or a public faucet).
Step 4 — Deploy plan
sunscreen deploy devnet --dry-run
Sunscreen prints a deploy plan: how much SOL you need, what's going to be uploaded. No on-chain action yet — it's a --dry-run.
When ready:
sunscreen deploy devnet
You'll get a program ID. Save it.
Step 5 — Mint
The Vite frontend in app/ is already wired with the Codama-generated client and the mint hook. Start it:
cd app
pnpm install
pnpm dev
Open http://localhost:5173 (Vite's default). Connect your wallet (the same one you funded), click Mint, approve. After a few seconds you'll see the mint signature.
You just minted an NFT through a program you wrote — even though sunscreen wrote most of it for you.
What happened end-to-end
quickstart nft → workspace + Metaplex recipe + frontend hooks
chain build → anchor build → IDL → Codama clients
wallet new / airdrop → fund a keypair on devnet
deploy devnet → upload program, register program ID
frontend pnpm dev → Vite app calls the generated client → mints
Next
- The dev loop with
chain serve— hot reload on save. - Deploying to mainnet — going from devnet to prod.
- Metaplex NFT recipe reference — what was generated, and why.
Rust primer (for Solana, in 10 minutes)
⏱ 10 min · 🎯 you'll understand: just enough Rust to read and edit Anchor programs.
You don't need to be a Rust expert to use sunscreen. You need to read code generated by scaffold and tweak it. This primer gives you exactly that.
The 30-second mental model
Rust is statically typed, has no garbage collector, and tracks who owns each value at compile time. In Anchor programs, the heavy machinery is hidden behind macros — #[program], #[derive(Accounts)], #[account]. You mostly write straight-line code inside instruction handlers.
Ownership (in 1 paragraph)
Every value has one owner. When the owner goes out of scope, the value is dropped (memory freed). You can lend a value out with &value (read-only) or &mut value (read-write). At any moment, you can have either many read-only borrows or one mutable borrow. The compiler enforces this — get used to error messages naming "borrow checker". For Anchor programs, this almost never bites you, because the framework manages account data.
Structs and impls
#![allow(unused)] fn main() { pub struct Counter { pub count: u64, } impl Counter { pub fn increment(&mut self) { self.count += 1; } } }
A struct holds fields. An impl block defines methods. &mut self means "this method can modify the struct".
Anchor's three macros
This is 90% of what you'll see in scaffolded code.
#[program]
#![allow(unused)] fn main() { #[program] pub mod my_app { use super::*; pub fn greet(ctx: Context<Greet>, name: String) -> Result<()> { msg!("Hello, {}!", name); Ok(()) } } }
The #[program] macro turns each pub fn into a callable instruction. Context<Greet> carries the accounts the instruction needs (declared next). Result<()> is Rust's standard error type — Ok(()) means success with no return value.
#[derive(Accounts)]
#![allow(unused)] fn main() { #[derive(Accounts)] pub struct Greet<'info> { #[account(mut)] pub user: Signer<'info>, pub system_program: Program<'info, System>, } }
This declares which accounts the instruction takes. mut = will be modified. Signer = must sign the transaction. The 'info is a lifetime — think of it as "these accounts live for the duration of this call". You'll rarely touch lifetimes manually.
#[account]
#![allow(unused)] fn main() { #[account] pub struct Counter { pub count: u64, pub authority: Pubkey, } }
#[account] marks a struct as on-chain storage. Anchor handles serialization/deserialization, plus a discriminator prefix.
Common types you'll see
| Type | What it is |
|---|---|
u8 .. u64, i8 .. i64 | unsigned/signed integers |
bool | true / false |
String | heap-allocated UTF-8 string |
Vec<T> | heap-allocated growable array of T |
Pubkey | 32-byte Solana public key |
Option<T> | Some(T) or None |
Result<T, E> | Ok(T) or Err(E) |
Error handling
In Anchor handlers you'll see:
#![allow(unused)] fn main() { require!(amount > 0, MyError::AmountZero); some_account.field = value; Ok(()) }
require! is a macro: if the condition is false, it returns Err(MyError::AmountZero). Otherwise execution continues. Ok(()) returns success.
The ? operator
#![allow(unused)] fn main() { let data = account.try_borrow_data()?; }
The ? says: if the call returned Err, propagate it up; if Ok, unwrap the value and continue. It's a shortcut for match.
What you can safely ignore (for now)
- Traits and generics beyond what Anchor uses.
- Async/await (Anchor programs are synchronous).
- Smart pointers (
Box,Rc,Arc) — Anchor doesn't need them in handlers. - Macros internals — just use them.
Going deeper
When you're ready: https://doc.rust-lang.org/book/ is the canonical, well-paced book. The chapters that matter for Anchor:
- Ch. 4: ownership + borrowing.
- Ch. 5–6: structs and enums.
- Ch. 9: error handling.
- Ch. 13: closures and iterators (useful for client code).
Next
- Solana primer — accounts, programs, PDAs, transactions.
- Your first workspace — apply this to read what sunscreen generates.
Solana primer (in 5 minutes)
⏱ 5 min · 🎯 you'll understand: accounts, programs, PDAs, fees, devnet/mainnet.
Everything on Solana is an account. Programs are accounts. Wallets are accounts. State is accounts. Once you internalize that, the rest follows.
Accounts
An account on Solana is a chunk of memory at an address (a 32-byte public key). It has:
- lamports: balance in the native token (1 SOL = 1,000,000,000 lamports).
- data: arbitrary bytes (program-defined layout).
- owner: the program that controls writes to this account.
- executable:
trueif the account is a program.
Two accounts of interest in your code:
- Wallet account: a keypair (you control). Holds SOL, signs transactions.
- Data account: holds program state (a counter, a user profile, an NFT mint).
Programs
A program is an account marked executable. Its data is BPF bytecode. Programs are stateless — they read and write other accounts. Your Anchor program is exactly this: a single executable account, deployed to an address (the program ID).
Transactions and instructions
You don't call a program directly. You build a transaction containing one or more instructions:
transaction
├── signers: [wallet]
├── instructions:
│ └── { program_id, accounts, data }
The runtime delivers the instruction to the program, which mutates the listed accounts and returns success or error.
PDAs (Program-Derived Addresses)
A normal address has a private key. A PDA does not — it's deterministically derived from seeds + a program ID, and only that program can sign for it. This is how programs "own" data accounts.
#![allow(unused)] fn main() { let (pda, bump) = Pubkey::find_program_address( &[b"counter", user.key().as_ref()], program_id, ); }
You'll see PDAs everywhere in Anchor: #[account(seeds = [...], bump)] declares them.
Fees
Every transaction pays a small fee in SOL, charged to the fee payer (usually the signer). On devnet this is essentially free. On mainnet, 5,000 lamports per signature is the base.
Account creation also costs rent: a one-time deposit proportional to the account's data size. You get it back when you close the account.
Networks
Three official networks plus your local validator:
| Network | URL | Use |
|---|---|---|
localnet | http://127.0.0.1:8899 | a validator on your laptop (sunscreen's chain serve launches one) |
devnet | https://api.devnet.solana.com | free SOL via faucet, public, test programs here first |
testnet | https://api.testnet.solana.com | validator testing, not for app dev |
mainnet | https://api.mainnet-beta.solana.com | production (Solana sometimes still calls this mainnet-beta in URLs) |
The development flow you'll use
- Write the program (sunscreen scaffolds it).
chain serve— runs againstlocalnetwith hot reload.sunscreen deploy devnet— push to devnet, test with real fees + real wallets.sunscreen deploy mainnet --yes-i-understand-cost— production.
Common pitfalls (the ones that bite everyone)
- You forgot to airdrop SOL before deploying to devnet. Run
sunscreen wallet airdrop. - Wrong program ID: after a deploy, your client must use the new ID. Sunscreen's Codama clients regenerate automatically.
- Account already initialized: PDAs are deterministic. If you
initthe same PDA twice, the second call fails. Useinit_if_neededor design for one-shot init. - Out of rent: closing an account refunds rent; not closing leaves SOL locked.
Want more?
The Solana docs are good: https://solana.com/docs. For Anchor specifically: https://www.anchor-lang.com/.
Next
- Your first workspace — apply this to a real program.
- Glossary — every term in one place.
Glossary
One- or two-sentence definitions for every term used elsewhere in the docs. Linked from primers and tutorials.
Solana
Account — addressable chunk of memory on Solana. Holds lamports, data, an owner program, and an executable flag. Everything on chain is an account.
Anchor — a Rust framework that hides Solana's low-level account boilerplate behind macros (#[program], #[derive(Accounts)], #[account]). Sunscreen targets Anchor by default.
BPF — Berkeley Packet Filter. The bytecode format Solana programs compile to.
Codama — a code generator that produces typed clients (JavaScript, Rust, …) from an Anchor IDL. Sunscreen wraps it.
Devnet — the public Solana test network. Free SOL via faucet, throwaway, perfect for staging before mainnet.
Discriminator — an 8-byte prefix Anchor writes at the start of every program-owned account, identifying its type.
Fee payer — the signer who pays transaction fees. Usually the wallet that initiated the transaction.
IDL (Interface Definition Language) — JSON file describing a program's instructions and account layouts. Anchor builds emit one; sunscreen exports it.
Instruction — a single call to a program inside a transaction. Specifies the program ID, the accounts touched, and a data payload.
Lamport — the smallest unit of SOL. 1 SOL = 10⁹ lamports.
Localnet — a local Solana validator running on your laptop. Sunscreen's chain serve launches one (via Surfpool or solana-test-validator).
Mainnet-beta — Solana production.
Metaplex — a collection of standards and tools for NFTs on Solana. The Token Metadata program is the canonical NFT spec.
PDA (Program-Derived Address) — an address derived deterministically from seeds + a program ID. Has no private key. Only the owning program can sign for it.
Pinocchio — a minimal-overhead alternative to Anchor for Solana programs. Sunscreen supports it via chain new --framework pinocchio.
Program — an executable account on Solana. Stateless. Reads and writes other accounts.
Program ID — the public key of a deployed program.
Pubkey — a 32-byte public key. Addresses, program IDs, signers — all Pubkeys.
Rent — a one-time deposit when creating an account, proportional to its data size. Refunded on close.
RPC — the JSON-RPC endpoint of a Solana validator (e.g. https://api.devnet.solana.com).
Signer — an account whose private key signed the transaction. Required for any account that gets debited or whose authority is asserted.
SOL — the native token. Used for fees and rent.
SPL Token — Solana's fungible token standard (and its program). Sunscreen's scaffold spl-token recipe generates a slice that mints / transfers.
Surfpool — a community-driven local validator alternative to solana-test-validator, optimized for dev loops. Sunscreen prefers it when present.
Transaction — a bundle of instructions, signed by one or more accounts, submitted to the network atomically.
Sunscreen
Chain (subcommand) — chain new, chain build, chain serve, chain doctor. Workspace lifecycle.
Codama config — a JSON file Sunscreen manages so Codama knows where to read the IDL and where to write clients.
Frontend hooks — React/Solid Query hooks Sunscreen generates from an IDL via generate frontend-hooks.
Generator tag — the value (e.g. account, event) Sunscreen writes inside a marker so re-runs know what to regenerate.
Marker — a comment pair (// sunscreen:begin {generator=…} … // sunscreen:end) inside generated files. Sunscreen edits only the content between them on subsequent runs. Read Marker protocol.
NDJSON events — newline-delimited JSON emitted by chain build / chain serve for editor and CI integration. See NDJSON events.
Plugin — an external binary speaking sunscreen's JSON-RPC protocol that extends scaffold <noun>. See Plugin protocol.
Recipe — a composite scaffold built on top of primitives. scaffold crud, scaffold spl-token, scaffold metaplex-nft.
Scaffold (subcommand) — adds an instruction, account, event, error, program, or recipe to an existing workspace.
Sunscreen.yml — the project's config file. Declarative source of truth for programs, plugins, runtime preferences.
Quickstart — beginner-friendly composite command that wires chain new + recipes + frontend hooks in one step.
Scaffolding a CRUD resource
⏱ 6 min · 🎯 you'll have: a complete Post resource with create/read/update/delete instructions, events, errors, and TS test.
The CRUD recipe is the fastest way to add a new resource to an existing program. It composes the primitive scaffolders (account, instruction, event, error) into a coherent slice.
Pre-requisites
- A workspace already created with
sunscreen chain new. - At least one program in
programs/.
Run
sunscreen scaffold crud Post --program my_app
Output (truncated):
✓ scaffolded account: Post
✓ scaffolded instructions: create_post, read_post, update_post, delete_post
✓ scaffolded events: PostCreated, PostUpdated, PostDeleted
✓ scaffolded errors: PostNotFound, PostUnauthorized
✓ tests/post.spec.ts
What was generated
| File | Purpose |
|---|---|
programs/my_app/src/state/post.rs | Post account struct with #[account] |
programs/my_app/src/instructions/create_post.rs | create_post handler |
programs/my_app/src/instructions/read_post.rs | read_post handler |
programs/my_app/src/instructions/update_post.rs | update_post handler |
programs/my_app/src/instructions/delete_post.rs | delete_post handler |
programs/my_app/src/events.rs (patched) | PostCreated, PostUpdated, PostDeleted events |
programs/my_app/src/errors.rs (patched) | PostNotFound, PostUnauthorized variants |
tests/post.spec.ts | TypeScript test scaffolding the four ops |
All writes happen inside marker regions. Hand-edit anywhere outside the markers and your changes survive future scaffolds.
Options
sunscreen scaffold crud Post \
--program my_app \
--fields "title:string,body:string,author:pubkey,created_at:i64" \
--frontend-hook \
--json
| Flag | Default | What it does |
|---|---|---|
--program <name> | required | program to scaffold into |
--fields "<spec>" | title:string,body:string | comma-separated name:type pairs for the account struct |
--frontend-hook | off | also generate React Query hooks (requires frontend: react in sunscreen.yml) |
--dry-run | off | print what would change without writing |
--json | off | machine-readable summary |
--force | off | overwrite existing conflicting symbols |
Idempotency
Re-running the same command is a no-op:
sunscreen scaffold crud Post --program my_app
# error: account "Post" already exists in program "my_app" (exit 4)
Add --force if you really want to regenerate (will not clobber hand-edits outside markers).
What "fields" supports
| Spec syntax | Anchor type |
|---|---|
name:string | String |
name:bool | bool |
name:u64 (also u8, u16, u32, u128) | matching unsigned int |
name:i64 (also i8, i16, i32, i128) | matching signed int |
name:pubkey | Pubkey |
name:vec<u8> | Vec<u8> (limited to 256 bytes) |
Complex types (nested structs, large vectors) need manual editing.
Build and test
sunscreen chain build
anchor test
The generated tests/post.spec.ts exercises each of the four operations end-to-end against a local validator.
Going further
- Recipe reference: CRUD — every flag, every generated file.
- SPL Token recipe and Metaplex NFT recipe — other composite recipes.
- Marker protocol — understand how regenerations stay safe.
The dev loop with chain serve
⏱ 6 min · 🎯 you'll have: a single-command dev loop that rebuilds, regenerates clients, and notifies your frontend on every save.
chain serve is sunscreen's supervised dev process. It runs a local validator, watches your files, and orchestrates everything else.
What it runs
┌─────────────────────────────────────────┐
│ chain serve │
│ │
│ ┌──────────┐ ┌──────────┐ ┌────────┐ │
│ │ validator│ │ watcher │ │pipeline│ │
│ │ (Surfpool│ │ (notify)│→ │anchor │ │
│ │ or t-v) │ │ debounce│ │ build │ │
│ └──────────┘ └──────────┘ │ ↓ │ │
│ │codama │ │
│ │ ↓ │ │
│ │frontend│ │
│ │notify │ │
│ └────────┘ │
└─────────────────────────────────────────┘
Start
From your workspace root:
sunscreen chain serve
The TUI shows four panels: validator status, build log, faucet, frontend status. Press ? for keybindings, q to quit.
Headless (CI / editor integration):
sunscreen chain serve --headless
Headless mode emits one NDJSON event per line on stdout. See NDJSON events.
What "watching" means
Sunscreen watches your workspace tree minus a list of ignored paths (target/, node_modules/, app/.sunscreen/, hidden dirs). When you save a Rust file, sunscreen:
- Debounces (waits ~200ms for related saves).
- Runs
anchor build. - If build succeeded and a frontend is configured, runs Codama to regenerate clients.
- Touches
app/.sunscreen/reloadso your frontend dev server (Vite, Next, …) hot-reloads.
If any step fails, the TUI shows the error and the validator stays up — fix and save again.
Choose your runtime
Sunscreen reads runtime.engine from sunscreen.yml. Override per-invocation with --runtime:
sunscreen chain serve --runtime test-validator
sunscreen chain serve --runtime surfpool
If Surfpool is the default but missing, sunscreen falls back to solana-test-validator automatically and logs the fallback.
Skip Codama on rebuild
If you don't have a frontend and don't need clients:
sunscreen chain serve --no-codama
A typical session
sunscreen chain serve— TUI appears, validator boots in ~2s.- Open
programs/my_app/src/instructions/create_post.rs, edit a handler. - Save. Within ~3s: anchor builds, Codama regenerates
app/src/clients/, your Vite dev server reloads. - Test in browser. Fix bugs. Repeat.
qto quit. Sunscreen tears down the validator, kills the process group cleanly.
Ctrl-C
Ctrl-C shuts everything down. Sunscreen stops the Unix process group of the validator (and any subprocess it spawned). If something doesn't exit within a grace period, sunscreen sends SIGKILL.
When it doesn't help
- You don't have an Anchor or Pinocchio workspace.
chain serverequires one. - You're on Windows. Process-group teardown isn't yet implemented for Windows; Phase 8 work.
- You want to manage a remote validator.
chain serveis for local dev only.
Going further
chain servereference — every flag.- NDJSON events — for editor integrations.
- Build pipeline — what runs and in what order.
Deploying to devnet
⏱ 6 min · 🎯 you'll have: your program deployed on Solana devnet, with the program ID wired into your config.
Pre-requisites
- Built workspace (
sunscreen chain buildsucceeded). solanaCLI on PATH (solana --version).- A Solana keypair, funded with devnet SOL.
Step 1 — Wallet
If you don't have a keypair yet:
sunscreen wallet new dev
This writes a new keypair under .sunscreen/wallets/dev.json and prints the public key. Save the recovery words if prompted.
Make it the default for localnet/devnet so subsequent commands pick it up:
sunscreen wallet set-default dev --cluster devnet
Step 2 — Airdrop devnet SOL
You need ~2 SOL for a fresh deploy:
sunscreen wallet airdrop 2 --cluster devnet
If you see Network: rate-limited, the public faucet throttled you. Options:
- Wait a few minutes and retry.
- Use a public web faucet: https://faucet.solana.com/.
- Run
solana airdrop 2 --url devnetdirectly.
Check the balance:
sunscreen wallet balance --cluster devnet
Step 3 — Deploy plan (dry run)
Always inspect the plan first:
sunscreen deploy devnet --dry-run
The dry run prints the planned Anchor invocation, the wallet that will pay, and the balance check.
Step 4 — Deploy
sunscreen deploy devnet
Or deploy only one program in a multi-program workspace:
sunscreen deploy devnet --program my_app
On success Anchor updates Anchor.toml with the new program ID. Sunscreen surfaces the result and the program addresses.
Step 5 — Sanity check
solana program show <program-id> --url devnet
You should see the program account with the correct authority and data length.
Step 6 — Regenerate clients
If you have a frontend:
sunscreen generate clients
This rewrites clients/<program>/ against the now-deployed program ID. Restart your pnpm dev so it picks up the new clients.
Re-deploying after code changes
Each subsequent deploy is incremental:
sunscreen chain build
sunscreen deploy devnet
Solana's solana program deploy handles the upgrade in place, reusing the program ID.
Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
insufficient funds | not enough SOL | airdrop or use the web faucet |
program too large | binary > buffer size | check --max-len on solana program deploy, or upgrade in chunks |
transaction simulation failed | program-side check failed | run the test suite locally first |
BlockhashNotFound | RPC overloaded or clock skew | retry; consider a private RPC (Helius, Triton) |
Next
- Deploying to mainnet — the same flow with more caution.
- Working with plugins.
deployreference.
Deploying to mainnet
⏱ 10 min · 🎯 you'll have: your program live on mainnet, with the same shape as your devnet deploy.
Mainnet is real money. Read this whole page before running anything.
Pre-flight checklist
-
Tests pass locally (
anchor test). - Deployed to devnet first and tested end-to-end.
- You have a dedicated mainnet keypair, separate from your dev wallet.
- That keypair has ~3 SOL (typical Anchor program deploy is 1.5–2.5 SOL).
- You're using a private RPC (Helius, Triton, QuickNode). Public mainnet RPCs throttle aggressively.
- Your program's upgrade authority is your control (a multisig, ideally).
Step 1 — Configure your wallet and RPC
Create or import a mainnet wallet and make it the default for mainnet:
sunscreen wallet new prod
sunscreen wallet set-default prod --cluster mainnet
Sunscreen passes through to the Solana CLI for RPC selection. Set a private endpoint in your shell or solana config:
solana config set --url https://your-rpc-endpoint.example.com
Step 2 — Dry run
sunscreen deploy mainnet --yes-i-understand-cost --dry-run
--yes-i-understand-cost is required for mainnet — sunscreen refuses to run without it. The --dry-run makes this safe: it prints the plan without sending transactions.
Read the plan carefully:
- Confirm the payer is your mainnet wallet, not your devnet one.
- Confirm the program count matches what you intend.
- Confirm your balance is enough.
If anything looks off, stop and investigate. Mainnet deploys do not refund "I clicked too fast".
Step 3 — Deploy
sunscreen deploy mainnet --yes-i-understand-cost
The CLI builds (if target/deploy/*.so is missing or stale), then runs anchor deploy --provider.cluster mainnet. A typical deploy takes 30–90 seconds per program, depending on RPC.
Step 4 — Verify
solana program show <program-id> --url mainnet-beta
Confirm the program is owned by BPFLoaderUpgradeab1e11111111111111111111111 and the upgrade authority is what you expect.
If you want users to verify the source matches the deployed bytecode, publish the program with Solana verifiable builds (sunscreen doesn't automate this yet).
Step 5 — Lock down upgrade authority
By default, the deploying keypair becomes the upgrade authority. For production, transfer it to a multisig (e.g. Squads) or, if you don't want anyone to upgrade, set it to None:
solana program set-upgrade-authority <program-id> --final
--final is irreversible. The program can never be upgraded again. Only do this for programs that you've audited and tested exhaustively.
sunscreen generate clients
Commit the regenerated clients. Your frontend now points at mainnet program IDs.
Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
RPC error: 429 Too Many Requests | public RPC throttled | use a private RPC via solana config set --url |
BlockhashNotFound mid-deploy | RPC dropped you | retry; Anchor's deploy is resumable |
| Wrong wallet used | environment variable leaked | inspect solana config get before deploying |
| Forgot to update IDL | clients have stale shape | sunscreen generate clients and redeploy frontend |
Going further
deployreference- Squads multisig docs — to manage upgrade authority.
- Helius RPC or Triton for private endpoints.
Working with plugins
⏱ 7 min · 🎯 you'll: install a local plugin, list its commands, run one, and understand the trust model.
Plugins extend sunscreen's scaffold <noun> surface. They are external binaries speaking sunscreen's JSON-RPC protocol over stdio. Use them to add team-specific scaffolds (custom CRUD shapes, internal protocol patterns, indexer hooks).
Plugin model in one paragraph
A plugin is a binary that, when invoked, reads JSON-RPC requests from stdin and writes JSON-RPC responses to stdout. Sunscreen calls the plugin's commands method to learn what it offers, then run when the user invokes one. Plugins are sandboxed: they can only write within the workspace and cannot reach the network unless declared.
Discover
sunscreen app marketplace
Lists reference plugins (sunscreen-apps/spl-token-2022, sunscreen-apps/yellowstone-indexer) and any plugins you've already installed.
Install a local plugin
Sunscreen reads plugins declared in sunscreen.yml:
plugins:
- source: ./plugins/my-plugin
version: "0.2.0"
Or install via CLI (idempotent, writes to sunscreen.yml):
sunscreen app install ./plugins/my-plugin --version 0.2.0
After install:
sunscreen app list
# my-plugin 0.2.0 ./plugins/my-plugin (status: declared)
status: declared means sunscreen knows about it. The first time you run a plugin command, sunscreen verifies the binary, reads its manifest, and starts a JSON-RPC session.
List a plugin's commands
sunscreen app commands my-plugin
# scaffold:
# indexer <Name> Scaffold a Yellowstone indexer slice
# listener <Name> Scaffold an event listener
Run a plugin command
sunscreen app run my-plugin -- scaffold indexer Trades
# or, when the plugin registers a top-level scaffold noun:
sunscreen scaffold indexer Trades --program my_app
Trust and sandbox
Sunscreen's plugin runtime enforces:
- Filesystem: writes restricted to the workspace root and below. Reads outside the workspace are allowed only for plugin-declared paths in its manifest.
- Network: blocked by default. Plugins must declare network needs in their manifest (
needs_network: true) and the user must approve at install time. - Subprocess: blocked. Plugins cannot spawn child processes.
If a plugin violates the sandbox, sunscreen kills the session and exits with code 9 (plugin_runtime). See Errors & exit codes.
Uninstall
sunscreen app uninstall my-plugin
Removes the entry from sunscreen.yml. Does not delete the plugin binary from disk (you do that manually).
Update
sunscreen app update my-plugin --version 0.3.0
Bumps the version in sunscreen.yml.
Hooks
Plugins can register hooks for build/serve events. Configure:
plugins:
- source: ./plugins/coverage
version: "0.1.0"
hooks:
- after_build
After every chain build, sunscreen calls the plugin's hook method with the build context. Use this for coverage reports, telemetry, custom artifacts.
Writing your own plugin
See the Plugin protocol reference for the JSON-RPC schema, manifest format, and gRPC contract.
Going further
appreference — all subcommands.- Plugin protocol — wire format.
- Plugin runtime concepts — sandbox model.
Troubleshooting
Top issues you'll run into, with concrete fixes. Sunscreen errors include a next_step hint by default — this page is the longer form of those hints.
toolchain_missing: anchor
Sunscreen needs anchor for chain build, chain serve, and most scaffold operations.
Fix: install via AVM:
cargo install --git https://github.com/coral-xyz/anchor avm --locked
avm install latest
avm use latest
Then sunscreen doctor to confirm.
toolchain_missing: solana
Needed for deploy, wallet airdrop, and chain serve --runtime test-validator.
Fix:
sh -c "$(curl -sSfL https://release.solana.com/stable/install)"
invalid_config (exit 3)
sunscreen.yml failed schema validation. The error message names the offending field.
Fix: run sunscreen doctor --json to see the parsed config, and compare to the schema reference. Common mistakes:
- Misspelled keys (
frontend: ractinstead ofreact). - Missing required field for a chosen variant (e.g.
framework: anchorrequires aprograms:list). - Version string without semver shape.
user_input (exit 4)
You asked sunscreen to do something that conflicts with the current state — e.g. scaffold an instruction that already exists, or install a plugin twice.
Fix: re-read the message. Sunscreen refuses to clobber. Pass --force only when you understand what gets overwritten.
missing_workspace (exit 5)
You ran a workspace-scoped command from a directory that has no sunscreen.yml upward.
Fix: cd into a sunscreen workspace, or sunscreen chain new first.
plugin_runtime (exit 9)
A plugin crashed or violated the sandbox.
Fix:
- Run
sunscreen app run <plugin> -- --helpto confirm the binary responds. - Check the plugin manifest declares the network/filesystem permissions it actually needs.
sunscreen app describe <plugin>shows the manifest sunscreen sees.
chain build succeeds but Codama fails
Common when the IDL changed shape in a way that breaks Codama's config.
Fix:
sunscreen generate clients --rebuild-config
This regenerates the Codama config from the IDL. Then re-run chain build.
chain serve doesn't pick up file saves
Usually one of:
- The file is inside a path sunscreen ignores (e.g.
target/,node_modules/,app/.sunscreen/). - macOS: too many files in the watch tree exceeds the descriptor limit. Run
ulimit -n 4096. - Linux: inotify limit.
echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p.
Frontend doesn't hot reload
Sunscreen writes app/.sunscreen/reload after Codama runs. Your dev server must watch that file.
Fix: for Vite, add to vite.config.ts:
server: {
watch: { ignored: ['!**/.sunscreen/reload'] }
}
For Next.js, place a useEffect polling the file mtime, or use the included useReload hook from generate frontend-hooks.
airdrop is rate-limited
The public devnet faucet throttles aggressively.
Fix: use https://faucet.solana.com/ (web), or a private RPC's faucet (Helius, QuickNode), or wait 10 minutes.
"I forgot to deploy after a code change"
sunscreen chain build && sunscreen deploy devnet
Sunscreen detects out-of-date target/deploy/*.so and rebuilds automatically before deploy.
Still stuck?
- Search existing issues: https://github.com/Pantani/sunscreen/issues.
- Open a new issue with
sunscreen doctor --jsonoutput and the command you ran.
CLI overview
sunscreen [GLOBAL_FLAGS] <COMMAND> [ARGS] [FLAGS]
Top-level commands
| Command | What it does |
|---|---|
chain new | Create a new workspace (Anchor or Pinocchio) |
chain build | Run anchor build + Codama regeneration |
chain serve | Supervised dev loop with local validator |
chain doctor | Diagnose toolchain + workspace markers |
scaffold | Add instruction, account, event, error, program, or recipe |
generate | Generate IDL, Codama clients, frontend hooks |
app | Manage plugins (install, list, run, hook, marketplace) |
doctor | Detect installed toolchain versions |
init | Interactive wizard for new users |
examples | Browse embedded example projects |
quickstart | Composite recipe shortcuts (token, nft, dao, blog) |
wallet | Wallet helpers (new, airdrop) |
deploy | Deploy programs to a network |
learn | Open a topic in the embedded learn index |
Global flags
| Flag | What it does |
|---|---|
--json | machine-readable output on stdout; human messages on stderr |
-v / -vv / -vvv | verbosity (warn / info / debug) |
--workdir <DIR> | override working directory |
--config <FILE> | path to an alternative sunscreen.yml |
--help | per-command help |
--version | print sunscreen version |
Exit codes
| Code | Meaning |
|---|---|
0 | success |
1 | unexpected error (bug) — please report |
2 | toolchain missing (anchor, solana, cargo, pnpm, …) |
3 | invalid config (sunscreen.yml schema violation) |
4 | user input conflict (resource exists, ambiguous flag, …) |
5 | missing workspace (no sunscreen.yml upward from cwd) |
9 | plugin runtime failure |
Full list with next_step strings in Errors & exit codes.
Environment variables
Sunscreen prefers explicit flags over magic environment variables. Pass --config <FILE> and --workdir <DIR> as needed. Standard Rust env vars (RUST_LOG, RUST_BACKTRACE) apply to the binary as usual.
--json contract
When you pass --json, sunscreen guarantees:
- One JSON object per command on stdout, or NDJSON (one object per line) for streaming commands (
chain build --headless,chain serve --headless). - Human-readable status messages, errors, hints, and progress go to stderr — never mixed into stdout.
- Top-level shape:
{ "status": "ok"|"error", "code": "<machine-readable>", "data": {…}, "next_step": "<optional hint>" }. - Error objects include
error.code,error.message, anderror.next_step.
Machine integrations should always pass --json and parse stdout only.
chain
Workspace lifecycle: create, build, serve, doctor.
sunscreen chain deploy is a stub today (Phase 2+). For real deploys use the top-level sunscreen deploy.
new
sunscreen chain new <NAME> [FLAGS]
Create a new workspace at ./<NAME>/ (or at --path if provided).
| Flag | Default | Description |
|---|---|---|
--framework <name> | anchor | anchor or pinocchio |
--frontend <name> | none | none, vite, next |
--path <DIR> | ./<NAME> | output directory |
--dry-run | off | print the planned file list without writing |
Global flags (apply to every subcommand): --json, --verbose/-v, --workdir <DIR>, --config <FILE>.
Exit codes: 0 ok · 2 toolchain missing · 4 directory exists / invalid argument.
Examples:
sunscreen chain new my-app
sunscreen chain new my-app --framework anchor --frontend vite
sunscreen chain new bare --framework pinocchio
sunscreen chain new my-app --frontend next --path packages/program
build
sunscreen chain build [FLAGS]
Run the build pipeline: anchor build (or cargo build-sbf for Pinocchio), then Codama client regeneration when the workspace has a frontend.
| Flag | Default | Description |
|---|---|---|
--headless | off | NDJSON events on stdout suitable for CI logs |
--no-codama | off | skip Codama regeneration after a successful build |
Exit codes: 0 ok · 2 toolchain missing · 5 no workspace · build-tool exit codes preserved on failure.
NDJSON events (selection — full list in NDJSON events):
{"event":"build_start","framework":"anchor","programs":["my_app"]}
{"event":"build_progress","step":"anchor_build"}
{"event":"build_ok","programs":["my_app"],"duration_ms":4200}
serve
sunscreen chain serve [FLAGS]
Long-running supervised dev loop: validator + watcher + build pipeline + frontend notify.
| Flag | Default | Description |
|---|---|---|
--headless | off | NDJSON stream, no TUI |
--no-codama | off | skip Codama on rebuild |
--no-frontend | off | skip frontend reload notifications |
--runtime <name> | from sunscreen.yml | surfpool or test-validator; defaults to the workspace's runtime.engine, falling back to solana-test-validator when Surfpool is requested but missing |
--debounce-ms <N> | 150 | watcher debounce window |
Exit codes: 0 ok (Ctrl-C) · 2 toolchain · 5 no workspace · 4 invalid arg (e.g. --debounce-ms 0).
Termination: Ctrl-C sends SIGTERM to the validator's process group, waits, then SIGKILL.
doctor
sunscreen chain doctor [--fix-markers]
Workspace-level diagnostic: marker integrity across scaffolded files.
| Flag | Default | Description |
|---|---|---|
--fix-markers | off | reconstruct safe non-appendable markers (see Marker protocol) |
For toolchain diagnostics see the top-level doctor command.
Exit codes: 0 ok · 4 non-fixable marker drift detected.
scaffold
Add code to an existing workspace. Primitives + recipes.
sunscreen scaffold <NOUN> <NAME> [FLAGS]
All scaffold operations are idempotent (re-running with the same args is a no-op) and marker-aware (writes happen inside marker regions; hand-edits outside markers survive). See Marker protocol.
Common flags
| Flag | Default | Description |
|---|---|---|
--program <name> | required for most | which program to scaffold into |
--dry-run | off | print planned diffs without writing |
--json | off | machine-readable summary |
--force | off | overwrite marker content even if a symbol exists |
Primitives
instruction <Name>
Add an instruction handler.
sunscreen scaffold instruction CreatePost --program blog
Creates programs/blog/src/instructions/create_post.rs, registers it in lib.rs dispatch, and updates instructions/mod.rs.
account <Name>
Add an account struct.
sunscreen scaffold account Post --program blog \
--fields "title:string,body:string,author:pubkey"
--fields syntax: name:type[,name:type…]. See CRUD recipe for accepted types.
event <Name>
Add an event.
sunscreen scaffold event PostCreated --program blog \
--fields "post:pubkey,author:pubkey,timestamp:i64"
error <Name>
Add an error variant.
sunscreen scaffold error PostNotFound --program blog \
--message "Post not found"
program <Name>
Add a new program to an existing workspace.
sunscreen scaffold program governance
Creates programs/governance/, updates Cargo.toml workspace members, Anchor.toml [programs.localnet], and sunscreen.yml.
Recipes
Composite scaffolds that combine primitives:
| Noun | What it scaffolds |
|---|---|
crud <Name> | account + 4 instructions + 3 events + 2 errors + TS test |
spl-token <Name> | SPL Token mint/transfer slice |
metaplex-nft <Name> | Metaplex NFT mint with metadata + master edition |
Each recipe runs a dry-run preflight: if any of its constituent primitives would conflict, it bails before writing.
Plugin-provided nouns
When a plugin declares scaffold <noun> commands, sunscreen routes sunscreen scaffold <noun> through that plugin. List available plugin scaffolds:
sunscreen app commands --filter scaffold
Exit codes
| Code | When |
|---|---|
0 | success |
2 | toolchain missing (rare; only for recipes that compile-check) |
3 | invalid config |
4 | symbol already exists, ambiguous flag, or recipe preflight failure |
5 | not in a workspace |
9 | plugin runtime failure (plugin-routed nouns) |
generate
Generate artifacts from the IDL.
sunscreen generate <ARTIFACT> [FLAGS]
generate clients is implicitly called by chain build and chain serve. Use these subcommands directly when you want to regenerate without rebuilding the program.
clients
sunscreen generate clients [--program <NAME>]
Run Codama against the workspace IDL and write a JavaScript/TypeScript client.
| Flag | Default | Description |
|---|---|---|
--program <NAME> | first IDL | program to regenerate clients for |
Requires: pnpm on PATH (sunscreen drives Codama via pnpm exec codama).
idl
sunscreen generate idl [--program <NAME>] [--out-dir <DIR>]
Export a deterministic IDL into the workspace.
| Flag | Default | Description |
|---|---|---|
--program <NAME> | all built IDLs in target/idl | program to export |
--out-dir <DIR> | clients/idl | output directory relative to the workspace root |
The exported IDL is byte-stable between runs as long as the source hasn't changed.
frontend-hooks
sunscreen generate frontend-hooks [FLAGS]
Generate TanStack Query hooks from exported IDLs.
| Flag | Default | Description |
|---|---|---|
--program <NAME> | all built IDLs | program to generate hooks for |
--frontend-path <DIR> | from sunscreen.yml | required when the workspace was scaffolded with --frontend none |
--target <NAME> | from project config | react, solid, or all (React Query + Solid Query wrappers) |
For each instruction in the IDL, generates a hook (useCreatePost, useReadPost, …) wrapping the Codama client. Hooks handle transaction building, signing, and refetching account queries on mutation success.
Exit codes
| Code | When |
|---|---|
0 | success |
2 | pnpm or another required dependency missing |
3 | sunscreen.yml invalid |
5 | not in a workspace |
Tips
chain buildcallsgenerate clientsautomatically. Rungeneratedirectly when you've touched the IDL by hand or need clients without rebuilding the.so.- Re-runs are idempotent. Codama overwrites only files it owns.
app
Manage plugins.
sunscreen app <SUBCOMMAND> [ARGS]
Plugins are declared in sunscreen.yml under plugins:. Most app subcommands edit that file declaratively.
install
sunscreen app install <SOURCE> [--version <SEMVER>] [--dry-run] [--json]
Add a plugin entry to sunscreen.yml. Idempotent: re-running with the same source is a no-op.
| Arg / flag | Description |
|---|---|
<SOURCE> | local path (./plugins/foo) or git URL (github.com/org/foo.git) |
--version | semver string; with or without v prefix |
--dry-run | print planned change without writing |
--json | `{"status": "declared" |
Source normalization: github.com/org/foo.git → plugin name foo. Conflicting basenames → exit 4.
uninstall
sunscreen app uninstall <NAME>
Remove a plugin entry. Does not delete files from disk.
list
sunscreen app list [--json]
Print declared plugins:
NAME VERSION SOURCE STATUS
my-plugin 0.2.0 ./plugins/my-plugin declared
describe
sunscreen app describe <NAME> [--json]
Read the plugin manifest and report:
- name, version
- declared commands (with
scaffold:,hook:namespacing) - declared permissions (
needs_network,needs_workspace_write, declared file paths)
update
sunscreen app update <NAME> --version <SEMVER>
Bump the version field in sunscreen.yml.
commands
sunscreen app commands [<NAME>] [--filter <scaffold|hook>] [--json]
List commands offered by one or all plugins.
run
sunscreen app run <NAME> -- <PLUGIN_ARGS>...
Invoke the plugin's run JSON-RPC method with the trailing args. Sunscreen sets up the sandbox, opens stdio JSON-RPC, and proxies stdout/stderr.
hook
sunscreen app hook <NAME> <HOOK_NAME> [--json]
Manually trigger a registered hook. Normally hooks fire automatically (e.g. after_build on chain build).
marketplace
sunscreen app marketplace [--json]
List reference plugins maintained under sunscreen-apps/. Currently:
sunscreen-apps/spl-token-2022sunscreen-apps/yellowstone-indexer
This is a static, embedded list. Remote registry support is planned.
Exit codes
| Code | When |
|---|---|
0 | success |
3 | sunscreen.yml invalid |
4 | name conflict, missing source, version not semver |
5 | not in a workspace |
9 | plugin binary crashed, sandbox violation, or JSON-RPC protocol error |
See Plugin protocol for the JSON-RPC wire format.
doctor
Detect installed toolchain versions.
sunscreen doctor [--json]
Outputs a table of tools sunscreen knows how to detect, with their installed version and availability flag.
Detected tools
| Tool | Detected via |
|---|---|
rustc | rustc --version |
cargo | cargo --version |
anchor | anchor --version |
solana | solana --version |
cargo-build-sbf | cargo build-sbf --help |
pnpm | pnpm --version |
node | node --version |
codama | from local node_modules/.bin/codama |
surfpool | surfpool --version |
If a tool is missing, sunscreen reports available: false and a next_step hinting installation. Missing tools are only blocking when you actually run a command that needs them.
Human output
TOOL VERSION STATUS
rustc 1.79.0 ok
cargo 1.79.0 ok
anchor 0.30.1 ok
solana 1.18.18 ok
cargo-build-sbf 1.18.18 ok
pnpm 9.4.0 ok
node 20.13.1 ok
codama (not found) missing
surfpool (not found) missing
--json output
A flat array of ToolReport objects:
[
{"tool":"rustc","version":"1.79.0","available":true,"next_step":null},
{"tool":"anchor","version":"0.30.1","available":true,"next_step":null},
{"tool":"codama","version":null,"available":false,"next_step":"pnpm add -D codama in your frontend, or sunscreen will install on demand"}
]
Use this in CI to assert your runner has the expected toolchain.
Exit codes
| Code | When |
|---|---|
0 | always — doctor reports, it does not fail on missing tools. Inspect available per row. |
For workspace-marker diagnostics, see chain doctor.
Onboarding commands
Beginner-friendly shortcuts that compose other sunscreen commands. All require the onboarding feature (enabled by default in release builds).
init
sunscreen init [<NAME>] [FLAGS]
Interactive wizard that asks 3–5 questions and runs chain new under the hood.
| Flag | Default | Description |
|---|---|---|
--non-interactive | off | disable prompts and require flag-based input |
--from-preset <NAME> | none | preset to apply when no prompts are available |
--frontend <name> | vite | none, vite, next |
--path <DIR> | ./<NAME> | output directory |
--dry-run | off | print planned files without writing |
examples
sunscreen examples <SUBCOMMAND>
| Subcommand | What it does |
|---|---|
list [--tag <TAG>] | list embedded examples (optionally filtered) |
describe <NAME> | print one example's README |
use <NAME> [<PATH>] [--non-interactive] [--dry-run] | copy an example onto disk |
quickstart
sunscreen quickstart <RECIPE> [FLAGS]
Composite recipes for "I want a working X in 30 seconds".
| Recipe | What it builds |
|---|---|
token | Anchor workspace + SPL Token recipe |
nft | Anchor workspace + Metaplex NFT recipe |
dao | Anchor workspace + DAO voting scaffolds |
blog | Anchor workspace + CRUD Post resource |
| Flag | Default | Description |
|---|---|---|
--name <NAME> | prompted | project name (required in --non-interactive) |
--cluster <NAME> | localnet | localnet, devnet, mainnet — used for the generated next steps |
--non-interactive | off | disable prompts |
--frontend <name> | vite | none, vite, next |
--path <DIR> | ./<NAME> | output directory |
--dry-run | off | print planned operations without writing |
wallet
sunscreen wallet <SUBCOMMAND>
| Subcommand | What it does |
|---|---|
new [<NAME>] [--out <FILE>] [--no-bip39-passphrase] [--dry-run] | Generate a keypair. When --out is omitted, lands under .sunscreen/wallets/<NAME>.json |
list | List wallets discovered under .sunscreen/wallets/ |
airdrop [<AMOUNT>] [--cluster <NAME>] [--to <PUBKEY>] [--dry-run] | Request SOL. AMOUNT defaults to 1.0. --cluster defaults to devnet. --to defaults to the Solana CLI default keypair |
balance [<ADDRESS>] [--cluster <NAME>] | Print a wallet balance |
set-default <NAME> [--cluster <NAME>] | Set the default wallet path in sunscreen.yml for a cluster |
Examples:
sunscreen wallet new dev
sunscreen wallet airdrop 2 --cluster devnet
sunscreen wallet balance --cluster devnet
sunscreen wallet set-default dev --cluster localnet
deploy
sunscreen deploy <TARGET> [FLAGS]
Build and deploy programs to a Solana cluster.
| Arg / flag | Default | Description |
|---|---|---|
<TARGET> | required | localnet, devnet, or mainnet (positional, value-enum) |
--program <NAME> | all programs | pass through to Anchor for a single program |
--verify | off | run anchor verify after deploy when supported |
--yes-i-understand-cost | off | required for mainnet |
--dry-run | off | print deployment plan without running Anchor |
Exit codes: 0 ok · 2 toolchain · 4 invalid args / insufficient balance · 5 no workspace.
Examples:
sunscreen deploy devnet
sunscreen deploy devnet --program my_app --dry-run
sunscreen deploy mainnet --yes-i-understand-cost
learn
sunscreen learn [<TOPIC>]
Print an embedded topic in the terminal. Omit <TOPIC> to list available topics.
next_step contract
Every onboarding error includes a next_step field in JSON output and a final line in human output telling the user exactly what to do. The contract is tested in tests/errors_contract.rs and is part of sunscreen's stable surface.
Example error:
error: Network: rate-limited (exit 4)
next_step: Try the web faucet at https://faucet.solana.com/ or wait 10 minutes.
sunscreen.yml schema
Single source of truth for a workspace. Generated by chain new, read by every other command.
This page summarizes the schema. The authoritative shape lives in src/config/schema.rs and is emitted as JSON Schema via schemars. When in doubt, the Rust types win.
version: 1
project:
name: my-app
framework: anchor # anchor | pinocchio
frontend: vite # none | vite | next
toolchain:
# tool version pins; see ToolchainCfg in schema.rs
anchor: "^0.30"
scaffolding:
# marker + scaffolder preferences; see ScaffoldingCfg
programs:
- name: my_app
path: programs/my_app
program_id: ~ # filled by deploy
workspace:
# workspace-level layout knobs
clusters:
localnet:
url: http://127.0.0.1:8899
wallet: ~/.config/solana/id.json
devnet:
url: https://api.devnet.solana.com
wallet: ~/.config/solana/id.json
mainnet:
url: https://api.mainnet-beta.solana.com
wallet: ~/.config/solana/id.json
runtime:
engine: surfpool # surfpool | test-validator
port: 8899
faucet_sol: 100
plugins:
- source: ./plugins/my-plugin
version: "0.2.0"
Top-level keys
| Key | Type | Required | Description |
|---|---|---|---|
version | int | yes | schema version; sunscreen migrates older values automatically |
project | object | no | name, framework (anchor/pinocchio), frontend (none/vite/next) |
toolchain | object | no | external tool version pins |
scaffolding | object | no | scaffolder/marker behaviour knobs |
programs | array | no | one entry per program in the workspace |
workspace | object | no | workspace-level layout |
clusters | object | no | per-cluster RPC + wallet (localnet/devnet/mainnet) |
runtime | object | no | local dev runtime preferences |
plugins | array | no | declared plugins |
programs[]
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | snake_case program name |
path | path | yes | relative to workspace root |
program_id | string | no | filled by deploy; null until first deploy |
clusters.<name>
| Field | Type | Required | Description |
|---|---|---|---|
url | string | yes | RPC endpoint |
wallet | string | yes | path to the default keypair for this cluster |
Sunscreen ships defaults for localnet, devnet, and mainnet. The CLI accepts mainnet (not mainnet-beta) as the cluster target name; the underlying URL remains https://api.mainnet-beta.solana.com.
runtime
| Field | Type | Default | Description |
|---|---|---|---|
engine | enum | surfpool | surfpool or test-validator (sunscreen falls back from Surfpool to test-validator when Surfpool isn't on PATH) |
port | int | 8899 | validator RPC port |
faucet_sol | int | 100 | seed balance for the local runtime faucet |
plugins[]
| Field | Type | Required | Description |
|---|---|---|---|
source | string | yes | local path or git URL |
version | semver | yes | with or without v prefix |
Migrations
When sunscreen reads a sunscreen.yml with a lower version than the current binary supports, it migrates in-place. Migrations are deterministic.
Validation
sunscreen.yml is validated on every command. Failures exit with code 3 (invalid_config) and a message naming the offending field path.
Recipes
Composite scaffolds built on top of scaffold primitives.
Each recipe runs as a single command and produces a complete, working slice — account, instructions, events, errors, tests, optional frontend hooks. All operations are idempotent and marker-aware.
Available recipes
| Recipe | What it generates |
|---|---|
| CRUD | An account + 4 instructions (create/read/update/delete) + 3 events + 2 errors + TS test |
| SPL Token | An SPL Token mint + transfer slice |
| Metaplex NFT | A Metaplex NFT mint with metadata + master edition |
Recipe contract
Every recipe guarantees:
- Preflight dry-run before any write. If any underlying primitive would conflict, the recipe bails with exit
4and no partial writes. - Single JSON object under
--json. The summary contains every file touched and every symbol generated. - Idempotency. Same inputs → same outputs. Re-running is a no-op.
- Marker-safety. All writes happen inside marker regions. Hand-edits outside markers survive.
- Optional frontend coupling. When the workspace declares a frontend,
--frontend-hookregenerates the matching React/Solid hook.
Writing a custom recipe
Sunscreen doesn't expose user-defined recipes natively. The supported way to extend the recipe surface is via a plugin that registers scaffold <noun> commands. See Plugin protocol.
CRUD recipe
sunscreen scaffold crud <NAME> --program <PROGRAM> [FLAGS]
Flags
| Flag | Default | Description |
|---|---|---|
--program <name> | required | which program to scaffold into |
--fields <spec> | title:string,body:string | account fields |
--frontend-hook | off | also generate matching React/Solid hooks |
--dry-run | off | print plan only |
--json | off | machine-readable summary |
--force | off | overwrite marker content even if a symbol exists |
Field types
--fields accepts comma-separated name:type pairs:
| Spec | Anchor type | Notes |
|---|---|---|
name:string | String | UTF-8, max 256 bytes by default |
name:bool | bool | |
name:u8 … name:u128 | u8 … u128 | |
name:i8 … name:i128 | i8 … i128 | |
name:pubkey | Pubkey | |
name:vec<u8> | Vec<u8> | max 256 bytes |
name:option<u64> | Option<u64> | optional fields |
Types outside this set require hand-editing the generated account.
Generated files
For scaffold crud Post --program blog:
| Path | Status |
|---|---|
programs/blog/src/state/post.rs | created |
programs/blog/src/state/mod.rs | patched (marker) |
programs/blog/src/instructions/create_post.rs | created |
programs/blog/src/instructions/read_post.rs | created |
programs/blog/src/instructions/update_post.rs | created |
programs/blog/src/instructions/delete_post.rs | created |
programs/blog/src/instructions/mod.rs | patched |
programs/blog/src/lib.rs | patched (dispatch markers) |
programs/blog/src/events.rs | patched (3 new variants) |
programs/blog/src/errors.rs | patched (2 new variants) |
tests/post.spec.ts | created |
Generated instructions
| Instruction | Accounts | Effect |
|---|---|---|
create_post | [authority: Signer, post: Account<Post> (init), system_program] | initializes a Post PDA seeded by authority + name |
read_post | [post: Account<Post>] | view-only; emits PostRead if the read fee model is enabled (off by default) |
update_post | [authority: Signer, post: Account<Post> (mut, has_one = authority)] | updates fields, emits PostUpdated |
delete_post | [authority: Signer, post: Account<Post> (close = authority, has_one = authority)] | closes the account, emits PostDeleted |
PDA seeds: ["post", authority.key, name.as_bytes()].
Generated events
PostCreated { post: Pubkey, author: Pubkey, timestamp: i64 }PostUpdated { post: Pubkey, timestamp: i64 }PostDeleted { post: Pubkey, timestamp: i64 }
Generated errors
PostNotFound— account discriminator mismatch.PostUnauthorized— signer is not theauthorityon the account.
Frontend hook (--frontend-hook)
For React + React Query:
useCreatePost({ authority, name, fields });
useReadPost({ post });
useUpdatePost({ authority, post, fields });
useDeletePost({ authority, post });
Hooks invalidate the relevant queries on mutation success. Generated in app/src/hooks/post.ts.
Exit codes
| Code | When |
|---|---|
0 | success |
4 | preflight conflict (symbol already exists; pass --force to overwrite marker content) |
5 | not in a workspace |
SPL Token recipe
sunscreen scaffold spl-token <NAME> --program <PROGRAM> [FLAGS]
Generates an SPL Token mint+transfer slice inside an existing program.
Flags
| Flag | Default | Description |
|---|---|---|
--program <name> | required | which program to scaffold into |
--decimals <n> | 9 | mint decimals |
--frontend-hook | off | generate matching React/Solid hook |
--dry-run | off | print plan only |
--json | off | summary on stdout |
--force | off | overwrite marker content even on conflict |
Generated files
For scaffold spl-token MyToken --program app:
| Path | Status |
|---|---|
programs/app/src/state/my_token.rs | created (mint metadata account) |
programs/app/src/instructions/init_my_token.rs | created |
programs/app/src/instructions/mint_my_token.rs | created |
programs/app/src/instructions/transfer_my_token.rs | created |
programs/app/src/instructions/mod.rs | patched |
programs/app/src/lib.rs | patched (dispatch) |
programs/app/src/events.rs | patched |
programs/app/src/errors.rs | patched |
tests/my_token.spec.ts | created |
Generated instructions
| Instruction | Effect |
|---|---|
init_my_token | Creates the mint PDA, sets authority and decimals |
mint_my_token | Mints amount to a destination token account (CPI to spl-token) |
transfer_my_token | Transfers amount between token accounts |
Generated events
TokenInitialized { mint, authority, decimals }TokenMinted { mint, recipient, amount }TokenTransferred { from, to, amount }
Generated errors
MintUnauthorizedInsufficientBalance
Notes
- This recipe uses the classic SPL Token program (
Tokenkeg…). For SPL Token-2022, install thesunscreen-apps/spl-token-2022plugin and usesunscreen scaffold spl-token-2022 …. - The mint is a PDA seeded by
["mint", name.as_bytes()], so the same name yields the same address per program.
Exit codes
| Code | When |
|---|---|
0 | success |
4 | preflight conflict |
5 | not in a workspace |
Metaplex NFT recipe
sunscreen scaffold metaplex-nft <NAME> --program <PROGRAM> [FLAGS]
Generates a Metaplex Token Metadata-compatible NFT mint slice.
Flags
| Flag | Default | Description |
|---|---|---|
--program <name> | required | which program to scaffold into |
--collection <name> | none | optional collection account name |
--frontend-hook | off | generate matching React/Solid hook |
--dry-run | off | print plan only |
--json | off | summary on stdout |
--force | off | overwrite marker content even on conflict |
Generated files
For scaffold metaplex-nft MyNft --program app:
| Path | Status |
|---|---|
programs/app/src/state/my_nft.rs | created |
programs/app/src/instructions/mint_my_nft.rs | created |
programs/app/src/instructions/mod.rs | patched |
programs/app/src/lib.rs | patched (dispatch) |
programs/app/src/events.rs | patched |
programs/app/src/errors.rs | patched |
tests/my_nft.spec.ts | created |
Generated instruction: mint_my_nft
Accounts:
[
authority: Signer,
mint: UncheckedAccount (init),
token_account: Account<TokenAccount> (init_if_needed),
metadata: UncheckedAccount (metaplex pda),
master_edition: UncheckedAccount (metaplex pda),
... + system_program, token_program, associated_token_program, rent
]
Effect:
- Creates a mint with 0 decimals (NFT convention).
- Mints 1 token to the authority's associated token account.
- CPI to Metaplex Token Metadata to create
metadata+master_edition. - Emits
NftMinted { mint, owner, uri }.
Generated events
NftMinted { mint, owner, uri }
Generated errors
MetadataUriTooLongMintAuthorityMismatch
Frontend hook (--frontend-hook)
useMintMyNft({ authority, uri, name, symbol });
Builds, signs, and submits the transaction. Invalidates the useMyNftCollection query on success.
Notes
- The recipe uses Metaplex Token Metadata via CPI. Your
Cargo.tomlgetsmpl-token-metadataadded as a dep on first scaffold. --collectionwires the minted NFT into a Metaplex collection account.
Exit codes
| Code | When |
|---|---|
0 | success |
4 | preflight conflict |
5 | not in a workspace |
Plugin protocol
Sunscreen plugins are external binaries that speak two transport layers:
- stdio JSON-RPC (default, used today).
- gRPC (defined in
proto/plugin.proto, planned for richer plugins).
Manifest
Each plugin ships a plugin.toml next to its binary:
[plugin]
name = "yellowstone-indexer"
version = "0.3.0"
description = "Scaffold a Yellowstone gRPC indexer slice."
entry = "./bin/plugin"
[permissions]
workspace_write = true # default: false
network = false # if true, prompts user at install time
declared_paths = [] # extra read-only paths outside workspace
[commands.scaffold]
indexer = "Scaffold a Yellowstone indexer slice"
listener = "Scaffold an event listener"
[commands.hook]
after_build = "Update indexer config when IDL changes"
JSON-RPC methods
Sunscreen calls these on the plugin via stdin/stdout. One JSON object per line.
commands
Request:
{"jsonrpc":"2.0","id":1,"method":"commands"}
Response:
{"jsonrpc":"2.0","id":1,"result":{
"scaffold": {"indexer": "…", "listener": "…"},
"hook": {"after_build": "…"}
}}
run
Request:
{"jsonrpc":"2.0","id":2,"method":"run","params":{
"command": "scaffold:indexer",
"args": ["Trades"],
"flags": {"program": "app"},
"workspace_root": "/abs/path/to/workspace",
"config": {"version": 1, "name": "my-app", "..." : "..."}
}}
Response (success):
{"jsonrpc":"2.0","id":2,"result":{
"files_written": ["programs/app/src/instructions/index_trades.rs", "..."],
"summary": "scaffolded indexer Trades"
}}
Response (error):
{"jsonrpc":"2.0","id":2,"error":{
"code": 4,
"message": "instruction index_trades already exists",
"data": {"next_step": "pass --force or pick a different name"}
}}
hook
Request:
{"jsonrpc":"2.0","id":3,"method":"hook","params":{
"name": "after_build",
"context": {"workspace_root": "...", "idl": {...}}
}}
Response: same shape as run.
Sandbox
Sunscreen enforces:
| Capability | Default | Toggle |
|---|---|---|
| Read workspace files | allowed | always on |
| Write workspace files | denied | permissions.workspace_write = true |
| Read outside workspace | denied | declare each path in permissions.declared_paths |
| Network access | denied | permissions.network = true (user-approved at install) |
| Spawn subprocesses | denied | not toggleable in current version |
Sandbox violations terminate the session and return exit 9 to the caller.
gRPC contract
proto/plugin.proto (in the sunscreen repo) defines the streaming-friendly equivalent of the JSON-RPC surface. Use when:
- Plugin needs bidirectional streaming (live progress, log forwarding).
- Plugin is implemented in a non-stdio-friendly runtime (e.g. JVM).
The gRPC transport is wire-defined but not yet end-to-end implemented in the runtime.
Conformance tests
Reference plugins under sunscreen-apps/ exercise the full protocol surface and serve as templates. The spl-token-2022 plugin is the canonical example.
NDJSON events
When you pass --headless (or --json to streaming commands), sunscreen emits one JSON object per line on stdout. Use this for editor integrations, CI parsers, and dashboards.
Event envelope
Every event has at least:
{
"event": "<name>",
"ts": "2026-06-02T14:33:12.412Z"
}
Plus event-specific fields.
Build pipeline events
Emitted by chain build and the build phase of chain serve.
| Event | When | Fields |
|---|---|---|
build_start | pipeline starts | framework, programs[] |
build_progress | step transition | step (anchor_build / cargo_build_sbf / codama) |
build_ok | pipeline succeeded | programs[], duration_ms |
build_fail | pipeline failed | step, exit_code, stderr_tail |
Codama events
| Event | When | Fields |
|---|---|---|
codama_start | Codama invocation begins | frontend |
codama_ok | clients written | files_written (int), duration_ms |
codama_fail | Codama failed | exit_code, stderr_tail |
Frontend notify
| Event | When | Fields |
|---|---|---|
frontend_notified | reload file touched | path |
Serve / watcher events
Emitted by chain serve --headless.
| Event | When | Fields |
|---|---|---|
serve_start | server ready | rpc_url, ws_url, validator |
validator_log | validator wrote a line | line (string) |
watcher_batch | file changes debounced | paths[] (relative), kind (save/delete) |
pipeline_triggered | watcher kicked a build | paths[] |
serve_stop | shutdown complete | reason (ctrl_c/error) |
Toolchain events
Emitted by any command that reaches a missing toolchain.
| Event | Fields |
|---|---|
toolchain_missing | tool, next_step |
Stability
The set of events listed here is part of sunscreen's stable API. Adding new events is non-breaking. Removing or renaming events is a breaking change handled via the SemVer policy in Roadmap.
Parsing tips
- Treat unknown events as informational. Do not fail on new event names.
eventis always a stable string. Use it as the discriminator.- Stdout is line-buffered; partial lines mean the process is still writing — wait for
\n. - Human messages may still hit stderr — don't merge stderr into your parser.
Errors & exit codes
Every sunscreen error has three things: a numeric exit code, a stable string code, and an actionable next_step.
Exit codes
| Exit | Code | Meaning |
|---|---|---|
0 | — | success |
1 | unexpected | a bug — please report with RUST_BACKTRACE=1 output |
2 | toolchain_missing | a required external tool (anchor, solana, …) was not found |
3 | invalid_config | sunscreen.yml failed schema validation |
4 | user_input | conflicting / ambiguous / unrecognized user request |
5 | missing_workspace | no sunscreen.yml found upward from cwd |
9 | plugin_runtime | a plugin crashed, violated the sandbox, or returned a protocol error |
Common error codes (string-level)
These are stable identifiers you can match on in scripts. Subset, full list lives in src/error.rs.
Toolchain (exit 2)
code | next_step |
|---|---|
toolchain_missing.anchor | Install via AVM: cargo install --git https://github.com/coral-xyz/anchor avm --locked && avm install latest |
toolchain_missing.solana | sh -c "$(curl -sSfL https://release.solana.com/stable/install)" |
toolchain_missing.pnpm | npm i -g pnpm |
toolchain_missing.cargo_build_sbf | Comes with the solana install — re-run the solana installer |
Invalid config (exit 3)
code | Trigger |
|---|---|
invalid_config.schema | Top-level shape doesn't match v1 |
invalid_config.field.<path> | A specific field is wrong (path follows the YAML, e.g. invalid_config.field.plugins.0.version) |
invalid_config.version_unknown | YAML declares a version newer than this binary supports |
User input (exit 4)
code | Trigger |
|---|---|
user_input.path_conflict | Target directory exists (sunscreen refuses to overwrite without --force) |
user_input.symbol_exists | A scaffold target (instruction, account, …) is already defined |
user_input.recipe_preflight | A recipe's preflight detected conflicts |
user_input.ambiguous_flag | Two flags conflict (e.g. --dry-run + --force) |
user_input.semver | --version argument isn't valid semver |
network.rate_limited | Faucet returned 429 |
network.unreachable | RPC didn't respond |
Missing workspace (exit 5)
code | Trigger |
|---|---|
missing_workspace | No sunscreen.yml in cwd or ancestors |
Plugin runtime (exit 9)
code | Trigger |
|---|---|
plugin_runtime.protocol | Plugin emitted invalid JSON-RPC |
plugin_runtime.sandbox | Plugin tried to write outside workspace or open a forbidden network connection |
plugin_runtime.crash | Plugin exited unexpectedly (non-zero, no response) |
plugin_runtime.manifest | Plugin manifest invalid or unreadable |
next_step contract
Every error includes a next_step field — a short, imperative sentence telling you what to do. This is part of the stable surface (tested by tests/errors_contract.rs).
Human-readable form:
error: user_input.symbol_exists: instruction "create_post" already exists (exit 4)
next_step: Pass --force to overwrite, or pick a different name.
JSON form:
{
"status": "error",
"error": {
"code": "user_input.symbol_exists",
"message": "instruction \"create_post\" already exists",
"next_step": "Pass --force to overwrite, or pick a different name"
}
}
Reporting bugs
Exit 1 (unexpected) is a bug. Include in the issue:
sunscreen --version- The full command you ran
- Output with
RUST_BACKTRACE=1 sunscreen doctor --json
Marker protocol
Markers are how sunscreen edits files it has generated, without clobbering your hand-edits.
Anatomy
#![allow(unused)] fn main() { // sunscreen:begin {generator=instructions} pub use create_post::*; pub use read_post::*; // sunscreen:end }
A marker is a region delimited by two comment lines:
// sunscreen:begin {generator=<tag>}— opens the region; carries the generator tag (dispatch,instructions,account,event,error,error_variants).// sunscreen:end— closes the region.
Sunscreen edits only the content between the markers. Everything outside is yours.
Why markers exist
When you re-run sunscreen scaffold instruction Foo, sunscreen needs to:
- Create a new file (
foo.rs). - Edit
instructions/mod.rsto re-export from it. - Edit
lib.rsto register the dispatcher.
Marker regions tell sunscreen where to inject in step 2 and 3, without re-parsing your whole file with syn.
Generator tags
| Tag | Lives in | Content |
|---|---|---|
dispatch | lib.rs | pub fn <ix>(...) -> Result<()> { ... } wrappers |
instructions | instructions/mod.rs | pub use <module>::*; re-exports |
state | state/mod.rs | pub mod <account>; declarations |
events | events.rs | pub use <event>::*; re-exports |
errors | errors.rs | pub use <error>::*; re-exports |
error_variants | inside an error enum | enum variants like #[msg("…")] Foo, |
Editing rules
You may edit outside markers. Sunscreen will not touch your changes.
You may not rely on hand-edits inside markers surviving a regeneration. The content of markers is regenerated from sunscreen's templates.
If you absolutely need a hand-customized version of generated code, copy it out of the marker into the unmanaged area below it.
Drift and repair
Sometimes a file gets into a state sunscreen can't safely edit (you deleted a marker; the file was scaffolded by an older sunscreen version; merge conflict left stray markers). Detect and repair:
sunscreen chain doctor
sunscreen chain doctor --fix-markers
--fix-markers only rebuilds markers when it can do so safely:
| Site | Repair behaviour |
|---|---|
instructions/mod.rs instructions marker missing | reconstruct, listing files present in instructions/ |
lib.rs dispatch marker missing | reconstruct only if all generated instruction files define pub fn handler (else refuse) |
errors.rs error_variants marker missing | reconstruct only if the enum body is empty or clearly delimited (else refuse) |
When --fix-markers refuses, you get an exit 4 with a description of the ambiguity and a manual fix path.
Implementation notes (for the curious)
Sunscreen's marker engine lives in src/rustpatch/. It uses line-based parsing (not AST) for resilience — even malformed Rust around the markers is fine as long as the marker pair is intact.
See also
- Incremental scaffolding — the mental model.
chain doctor— the repair command.
Architecture
Sunscreen is organized in four layers. Each one has a single responsibility and a clean interface to the next.
flowchart TB
User([User]) --> CLI[CLI surface<br/>clap commands]
CLI --> Runtime[Runtime layer<br/>watcher · supervisor · pipeline]
CLI --> Templates[Templates engine<br/>rust-embed · minijinja]
CLI --> Plugins[Plugin runtime<br/>stdio JSON-RPC · sandbox]
Runtime --> Tools[(External tools<br/>anchor · cargo-build-sbf<br/>codama · surfpool)]
Templates --> Workspace[(Workspace files<br/>programs/ · app/<br/>sunscreen.yml)]
Plugins --> Workspace
Runtime --> Workspace
Layer 1 — CLI surface
src/cli/. One module per top-level command (chain.rs, scaffold.rs, generate.rs, app.rs, onboarding/). Argument parsing via clap, exit codes from a single error.rs.
The CLI layer is thin: it parses, validates, then hands off to the runtime or templates layers.
Layer 2 — Runtime
src/runtime/. Owns:
- Subprocess management (
subprocess.rs) —CommandSpec,ProcessRunner,SubprocessRunner. Every shell-out goes through this for testability. - Build pipeline (
pipeline.rs) — anchor build → IDL export → Codama → frontend notify. - Watcher (
watcher.rs) —notifyevents, debounced, dedup'd, filtered. - Validator adapters (
surfpool.rs,testvalidator.rs) — abstracted behind aLocalValidatortrait. - Supervisor (
supervisor.rs,serve.rs) — long-running orchestrator forchain serve.
The runtime never reads CLI args directly. It receives configured *Spec structs.
Layer 3 — Templates
src/templates/ + templates/ (embedded via rust-embed).
Two responsibilities:
- Scaffold workspaces and primitives with deterministic
minijinjarendering. Golden-tested. - Patch existing files through
src/rustpatch/— the marker engine. Line-based, AST-free, resilient to malformed input.
Layer 4 — Plugin runtime
src/plugin/. Discovers plugins from sunscreen.yml, validates manifests, opens a stdio JSON-RPC session, enforces the sandbox, and routes scaffold <noun> commands when a plugin claims a noun.
The gRPC contract in proto/plugin.proto is wire-defined but not yet end-to-end live.
Cross-cutting
| Concern | Where |
|---|---|
Config (sunscreen.yml) | src/config/ — schema, loader, migrations |
| Toolchain detection | src/toolchain/ — uniform ToolReport for every external tool |
| Errors | src/error.rs — single Error enum, code + next_step |
| TUI (chain serve) | src/tui/serve_model.rs |
Design principles
- Determinism. Same inputs → same outputs. Golden tests cover scaffolds; round-trip tests cover marker patches.
- Boundary isolation. Every subprocess call goes through the runner; every file write is in the templates layer or the marker engine. Easy to mock for tests.
- Two-layer plugin model. JSON-RPC over stdio is the floor; gRPC is the ceiling. Both share the same logical contract.
- CLI is the API.
--jsonoutput is part of stability. Editor integrations don't need an SDK.
See also
- Build pipeline — what runs in what order.
- Incremental scaffolding — markers in depth.
- Plugin runtime — sandbox and trust.
Workspace model
A sunscreen workspace is a directory containing four things:
- A
sunscreen.ymlat the root. - A Rust workspace
Cargo.toml. - An
Anchor.toml(for Anchor) or nothing extra (for Pinocchio). - One or more programs under
programs/<name>/.
Optional:
- A frontend under
app/(React or Solid). - A
plugins/directory with local plugins. - A
tests/directory with TypeScript integration tests.
Anchor layout
my-app/
├── sunscreen.yml
├── Anchor.toml
├── Cargo.toml ← workspace member list
├── package.json ← TypeScript test dependencies
├── tsconfig.json
├── programs/
│ ├── core/
│ │ ├── Cargo.toml
│ │ └── src/
│ │ ├── lib.rs ← #[program], dispatch markers
│ │ ├── state/
│ │ ├── instructions/
│ │ ├── events.rs
│ │ └── errors.rs
│ └── governance/ ← multi-program workspaces are supported
├── app/ ← frontend (when --frontend != none)
│ ├── package.json
│ ├── vite.config.ts
│ ├── src/
│ │ ├── clients/ ← Codama-generated
│ │ ├── hooks/ ← from generate frontend-hooks
│ │ └── App.tsx
│ └── .sunscreen/
│ └── reload ← touch trigger for frontend HMR
└── tests/ ← TypeScript integration tests
Pinocchio layout
bare/
├── sunscreen.yml
├── Cargo.toml
└── programs/
└── bare/
├── Cargo.toml ← `cargo build-sbf` target
└── src/
├── lib.rs ← entrypoint! macro, instruction dispatch
└── instructions/
Pinocchio workspaces are minimal: no Anchor.toml, no built-in IDL. Codegen and recipes are Anchor-only today.
Multi-program workspaces
A workspace can declare multiple programs. Sunscreen treats each one independently for scaffold, but bundles them for build, deploy, serve:
programs:
- name: core
path: programs/core
- name: governance
path: programs/governance
- name: treasury
path: programs/treasury
sunscreen chain build compiles all three. sunscreen scaffold instruction Foo --program governance targets only the named one.
Discovery
When you invoke any workspace-scoped command, sunscreen walks up the directory tree from cwd looking for sunscreen.yml. Found → that's your workspace root. Not found → exit 5 (missing_workspace).
You can override with SUNSCREEN_CONFIG=/path/to/sunscreen.yml.
Lock files
Sunscreen does not write its own lock file. The Anchor/Cargo/pnpm lock files are the source of truth for dependency versions. Sunscreen's templates pin minor versions in Cargo.toml; you control the rest.
See also
Incremental scaffolding
The problem: you scaffold a program, then add an instruction six weeks later. You don't want sunscreen to regenerate lib.rs from scratch — that would clobber the body you wrote. You also don't want to maintain a hand-edited list of dispatch entries.
The solution: markers.
Lifecycle
sequenceDiagram
participant U as User
participant S as Sunscreen
participant F as File (lib.rs)
Note over F: initial state — markers present, empty
U->>S: scaffold instruction Foo
S->>F: read
S->>F: inject pub fn foo into "dispatch" marker
S->>F: write
Note over F: dispatch marker now contains foo handler
U->>F: hand-edit outside markers
Note over F: your edits live below the marker
U->>S: scaffold instruction Bar
S->>F: read
S->>F: inject pub fn bar into "dispatch" marker<br/>(your hand-edits untouched)
S->>F: write
Three guarantees
- Idempotency. Re-running with the same args is a no-op. Sunscreen detects "this symbol already exists in the marker" and exits
4unless--forceis passed. - Marker isolation. Content outside markers is never touched. Content inside markers may be fully regenerated on every run.
- Drift detection.
chain doctorfinds missing or duplicated markers and reports them.chain doctor --fix-markersrepairs safe drift (it refuses to repair sites where reconstruction would be ambiguous).
Where markers live
Sunscreen places markers in files it scaffolds. As of v0.1.0:
| File | Marker tag |
|---|---|
programs/*/src/lib.rs | dispatch |
programs/*/src/instructions/mod.rs | instructions |
programs/*/src/state/mod.rs | state |
programs/*/src/events.rs | events |
programs/*/src/errors.rs | errors |
Any error enum inside errors.rs | error_variants (inside the enum) |
Why not AST-based?
We tried. syn-based patching is brittle when the surrounding code is malformed (e.g. mid-edit, half-typed). Line-based marker editing tolerates anything as long as the marker pair is intact, and merges cleanly with editor undo histories.
The cost: you must keep the markers. chain doctor will remind you.
Read also
- Marker protocol reference — exact syntax, generator tags, repair rules.
chain doctor— repair command.
Build pipeline
sunscreen chain build (and the build phase of chain serve) runs a deterministic pipeline of stages. Each stage is gated on the previous one's success.
Stages
flowchart LR
A[anchor build<br/>or cargo build-sbf] --> B{exit 0?}
B -- no --> X[emit build_fail<br/>exit non-zero]
B -- yes --> C[IDL export<br/>idl/*.json]
C --> D{frontend declared?}
D -- no --> Z[emit build_ok<br/>exit 0]
D -- yes --> E[codama run<br/>app/src/clients/]
E --> F{exit 0?}
F -- no --> Y[emit codama_fail<br/>exit non-zero]
F -- yes --> G[touch app/.sunscreen/reload]
G --> Z
Stage 1 — Compile
- Anchor workspaces:
anchor build. Producestarget/idl/<program>.jsonandtarget/deploy/<program>.soper program. - Pinocchio workspaces:
cargo build-sbf. Produces only the.so(Pinocchio has no IDL emission yet).
Failure → emit build_fail, return Anchor's exit code (sunscreen preserves it).
Stage 2 — IDL export
For Anchor workspaces, sunscreen copies the IDL into idl/<program>.json (normalized, sorted, byte-deterministic). This is the file Codama and your CI consume.
Skipped for Pinocchio.
Stage 3 — Codama
Runs only if frontend != none in sunscreen.yml, and --no-codama was not passed.
Sunscreen manages a codama.config.mjs in the workspace root, pointing at the exported IDLs and the configured app/src/clients/ output. The command is:
pnpm exec codama run
Failure → emit codama_fail. Sunscreen does not roll back the IDL (the partial state is recoverable on the next successful build).
Stage 4 — Frontend notify
On success of stage 3, sunscreen touches app/.sunscreen/reload. Frontend dev servers (Vite, Next) watching this file trigger HMR.
If frontend != none but you don't have a dev server running, this is a harmless no-op.
NDJSON event sequence (success case)
{"event":"build_start","framework":"anchor","programs":["my_app"]}
{"event":"build_progress","step":"anchor_build"}
{"event":"build_ok","programs":["my_app"],"duration_ms":4200}
{"event":"codama_start","frontend":"react"}
{"event":"codama_ok","files_written":14,"duration_ms":850}
{"event":"frontend_notified","path":"app/.sunscreen/reload"}
chain serve extras
In serve, the pipeline runs every time the watcher emits a debounced batch:
sequenceDiagram
participant FS as Filesystem
participant W as Watcher
participant P as Pipeline
FS->>W: save event(s)
Note over W: debounce ~200ms
W->>P: run(paths)
P->>P: stages 1..4
P-->>W: result
Note over W: ready for next batch
Why this shape?
- Compile must precede everything. No IDL → nothing else can run.
- IDL is the protocol boundary. Once written, every downstream tool consumes it. Sunscreen does not feed Codama from in-memory IDL objects — the file is the contract.
- Codama is optional. Library projects without a frontend skip it. CI flags
--no-codamato keep builds fast. - Frontend notify is the last step. Any failure earlier and the frontend keeps showing the previous client.
See also
Plugin runtime
Sunscreen lets you extend the scaffold <noun> surface via external binaries. The runtime is intentionally minimal: stdio JSON-RPC, a small set of capabilities, an explicit sandbox.
Why plugins?
The CLI cannot ship every scaffold the world needs. Teams have internal conventions, third-party libraries have idiomatic shapes, indexer providers have their own templates. Plugins let those live close to where they're maintained, without forking sunscreen.
Lifecycle
sequenceDiagram
participant User
participant Sunscreen
participant Plugin
User->>Sunscreen: app install ./plugins/foo --version 0.2.0
Sunscreen->>Sunscreen: write entry to sunscreen.yml
Note over Sunscreen: status: declared
User->>Sunscreen: scaffold indexer Trades
Sunscreen->>Plugin: spawn binary in sandbox
Sunscreen->>Plugin: stdio: {method:"commands"}
Plugin-->>Sunscreen: {scaffold:{indexer:"…"}}
Sunscreen->>Plugin: stdio: {method:"run",params:{...}}
Plugin-->>Sunscreen: {files_written:[…]}
Sunscreen->>User: summary
Trust model
Plugins are arbitrary code. Sunscreen assumes nothing about a plugin's intent and enforces capabilities at the sandbox layer:
| Capability | Default | Opt-in mechanism |
|---|---|---|
| Read inside workspace | allowed | always on |
| Write inside workspace | denied | permissions.workspace_write = true in manifest |
| Read outside workspace | denied | declare each path in permissions.declared_paths[] |
| Network | denied | permissions.network = true + user approval at install |
| Subprocess spawn | denied | not toggleable in current version |
Violations terminate the session and return exit 9 (plugin_runtime.sandbox).
Discovery
Plugins are not magic. They live in sunscreen.yml:
plugins:
- source: ./plugins/foo # local
version: "0.2.0"
- source: github.com/org/bar.git # remote (download not implemented yet)
version: "1.0.0"
Sunscreen reads this on every command and refreshes its plugin registry.
JSON-RPC contract
Three methods over line-delimited stdin/stdout:
commands— returns the plugin's command map.run— invokes a single command with args, flags, and workspace context.hook— fires a lifecycle hook (before_build,after_build, …).
See Plugin protocol reference for full schemas.
gRPC
proto/plugin.proto defines a streaming-friendly equivalent. Useful for plugins that need:
- Bidirectional streaming (live progress, log forwarding).
- Non-stdio runtimes (JVM, .NET).
The proto is stable; the gRPC transport is not yet end-to-end live in sunscreen's runtime.
Reference plugins
sunscreen-apps/ contains canonical examples:
spl-token-2022— addsscaffold spl-token-2022 <Name>with the 2022-extension features.yellowstone-indexer— addsscaffold indexer <Name>wiring a Yellowstone gRPC consumer.
Read their code to learn the protocol from a working example.
When not to write a plugin
- The scaffold is generic enough to belong in core (open a PR).
- You only need a small variation on an existing recipe — consider a
--flaginstead. - Your team can just copy-paste a snippet (don't over-engineer).
See also
- Plugin protocol reference — wire format.
appcommand reference — operate on plugins.- Working with plugins guide — install/run walkthrough.
Anchor vs Pinocchio
Sunscreen supports two Solana program frameworks. Choose based on what you're optimizing for.
TL;DR
| Anchor | Pinocchio | |
|---|---|---|
| Mental model | High-level, macros do the heavy lifting | Low-level, you wire account checks yourself |
| Compile-time guarantees | Strong (#[derive(Accounts)] checks at build time) | Minimal (you assert at runtime) |
| Compute units | Higher overhead | Near-bare-metal |
| Code volume | Less code per instruction | More code per instruction |
| Sunscreen support | Full (scaffold, codegen, recipes, plugins) | Workspace bootstrap + build only |
| When to pick | New projects, productivity-first | Performance-critical paths, audit-sensitive code |
Anchor in one paragraph
Anchor is a Rust framework on top of the Solana program SDK. It defines three macros (#[program], #[derive(Accounts)], #[account]) that hide most of the account-handling boilerplate. It also auto-generates an IDL, which sunscreen feeds into Codama for client generation. Default choice for productivity.
Pinocchio in one paragraph
Pinocchio is a thin, no-allocation alternative for Solana programs. It exposes the runtime more directly, so you pay fewer compute units (CUs) per instruction. There's no IDL emission, no codegen, no Anchor macros — you write the dispatch and account validation by hand. Use for programs where CU budget matters.
Sunscreen's coverage today
| Feature | Anchor | Pinocchio |
|---|---|---|
chain new | ✅ | ✅ (minimal template) |
chain build | ✅ | ✅ (cargo build-sbf) |
chain serve | ✅ | ✅ |
scaffold instruction/account/event/error | ✅ | ❌ (manual) |
scaffold crud/spl-token/metaplex-nft | ✅ | ❌ |
generate idl | ✅ | ❌ (no IDL) |
generate clients | ✅ | ❌ |
generate frontend-hooks | ✅ | ❌ |
Plugin scaffold <noun> | ✅ | partial (plugins decide) |
If you choose Pinocchio, sunscreen still gives you the workspace bootstrap, build, and local dev loop. Scaffolders and codegen require Anchor's IDL.
Migrating between
There's no automatic migration. Anchor programs use macros and account layouts that don't map 1:1 to Pinocchio. If you want to migrate a hot path to Pinocchio, the typical pattern is:
- Keep the Anchor program as the user-facing surface.
- Extract the hot instruction into a separate Pinocchio program.
- Have the Anchor program CPI into the Pinocchio program.
See also
chain new --framework— choosing at creation time.- Anchor docs.
- Pinocchio repo.
Roadmap
The canonical, living roadmap is ROADMAP.md at the repo root. It is the source of truth for what's done, in progress, and queued.
This page summarizes the high-level shape so you can orient quickly.
Current state
| Phase | Status |
|---|---|
| 0 — Foundation (CLI shell, config, toolchain, templates, error) | ✅ closed |
1 — Workspace bootstrap (chain new for Anchor + frontend variants) | ✅ closed |
2 — Incremental scaffolders (scaffold instruction/account/event/error/program + doctor --fix-markers) | ✅ closed |
| 3 — Build/serve pipeline + watcher + runtime supervisor | ✅ closed |
4 — Codegen (generate clients/idl/frontend-hooks) | ✅ closed |
5 — Recipes (scaffold crud/spl-token/metaplex-nft) | ✅ closed |
5.5 — Onboarding surface (init, quickstart, wallet, deploy, learn) | ✅ closed |
| 6 — Plugin runtime (manifest, JSON-RPC, sandbox, marketplace) | ✅ closed |
7 — Pinocchio bootstrap (chain new --framework pinocchio + build) | ✅ closed |
| 8 — Distribution & docs (this site, completions, Homebrew, Windows) | in progress |
Phase 8 work items
- ✅ tag-driven
cargo-distrelease pipeline (Linux + macOS). - ✅ documentation site (this site).
- ⏳ shell completions (
bash,zsh,fish,pwsh). - ⏳ Homebrew tap.
- ⏳ Windows artifact (
cargo-distmatrix). - ⏳
cargo dist planCI verification. - ⏳
cargo-binstallsupport.
After v1.0
- Remote plugin artifact download.
- Pinocchio-native scaffolders.
- Richer marketplace (signed plugins, search).
- Codama provider abstraction (alternative codegen backends).
Semantic versioning
v0.x is a preview line. Breaking changes can happen between minor versions, always called out in CHANGELOG.md. v1.0 will lock the CLI surface and the JSON contracts. After v1.0, breaking changes require a major bump.
How to contribute to the roadmap
- For larger work: open a discussion or draft an ADR (see Architecture decisions).
- For Phase 8 items: pick an unticked box above, comment on the issue, ship a PR.
- For polish/docs: PRs are welcome on
docs/site/without an issue.
Architecture decisions
Sunscreen's design rationale lives in Architecture Decision Records (ADRs) in docs/adr/.
Current ADRs
| ID | Title | Status |
|---|---|---|
| ADR-0001 | The sunscreen CLI: scope, vision, principles | Accepted |
| ADR-0002 | CLI design conventions (exit codes, flags, JSON contract) | Accepted |
| ADR-0003 | Documentation strategy | Accepted |
| ADR-0004 | Incremental scaffolding (markers) | Accepted |
| ADR-0005 | Beginner onboarding surface | Accepted |
| ADR-0006 | Pinocchio bootstrap | Accepted |
ADR format
Each ADR has:
- A meta table (status, date, authors, tags, supersedes, related).
- TL;DR.
- Context.
- Decision drivers.
- Considered options.
- Decision.
- Consequences.
Template: see ADR-0001.
Writing a new ADR
Open a PR adding docs/adr/ADR-NNNN-<slug>.md, status Proposed. Discuss in the PR. When merged, status flips to Accepted. If a later ADR overrides it, set Status: Superseded by ADR-NNNN rather than deleting.
ADRs document why we chose what we chose, not how to use the code. For user-facing docs, contribute to this site instead.
Developer setup
How to clone, build, and test sunscreen itself.
Pre-requisites
- Rust stable (matches
rust-toolchain.tomlif present). - For the integration tests that exercise Anchor:
anchor,solana,cargo-build-sbf,pnpm, Node 18+. - The CI runner does not need Anchor/Solana — Anchor-touching tests are
#[ignore]by default and explicitly gated.
Clone and build
git clone https://github.com/Pantani/sunscreen.git
cd sunscreen
cargo build --locked --release --all-features
The release binary lands at target/release/sunscreen. Symlink it onto your PATH if you want to use the locally-built version.
Run the test suite
# Fast unit + golden + integration smoke (no Anchor toolchain required)
cargo test --locked --all --all-features --no-fail-fast
# Feature-gate check (`--no-default-features` must compile)
cargo check --locked --no-default-features --all-targets
# Lints
cargo fmt --all -- --check
cargo clippy --locked --all-targets --all-features -- -D warnings
Anchor-touching tests
These are ignored by default. To run them you need the full Solana stack:
cargo test --locked --test integration_anchor -- --include-ignored
If your toolchain is missing parts, the tests skip with a clear message rather than fail.
Editor
Any editor that speaks rust-analyzer works. The repo includes no editor-specific config — open the workspace root and rust-analyzer figures out the rest.
Pre-commit
The repo expects cargo fmt + cargo clippy --no-warnings before commits. CI fails the PR otherwise.
Where to start
src/cli/— to add a flag or subcommand.src/templates/+templates/— to change scaffold output.src/runtime/— for build/serve pipeline behavior.tests/golden/— snapshot tests for scaffolds.
Docs
To preview this site locally:
cargo install mdbook mdbook-admonish mdbook-mermaid mdbook-linkcheck
mdbook serve docs/site --open
Releases
Releases are tag-driven: push vX.Y.Z, the release.yml workflow builds and publishes binaries. The docs site auto-deploys from main via .github/workflows/docs.yml.
Documentation style
Rules of thumb for writing pages on this site. Apply them as defaults; break them when the topic actually needs it.
Tracks have purposes
- Learn — teaches concepts to newcomers. Defines every term on first use. Linear reading.
- Guides — solves one specific task. Has a tangible artifact at the end. Time-boxed (declare
⏱). - Reference — catalogs. Optimized for
Ctrl-F. Tables and lists over prose. - Concepts — explains why. The model behind the code. Pairs with reference pages.
Don't blur tracks. If a page is doing two jobs, split it.
Tone
- Plain, present, active. "Sunscreen runs
anchor build." Not "It is the case that sunscreen will runanchor build." - Second person for tutorials and guides. "You'll have a workspace."
- Third person for reference. "The command returns exit 4 when…"
- Honest. If a feature is partial, say so. If a step might fail, say what to do.
Length
- Aim for short sentences. Long sentences are usually two short ones in a trenchcoat.
- A Learn page: 200–500 lines, paced with subheads every ~50 lines.
- A Reference page: as long as needed; structure with tables, not prose.
- A Concept page: 100–300 lines with a diagram if it helps.
Code blocks
Every code block on a Learn or Guide page must run against main exactly as written. Pre-commit: run the command yourself, paste the output.
For Reference, code blocks may show the shape of input/output without being directly runnable — but call that out (e.g. "request body shape:").
Diagrams
Use Mermaid for diagrams. Embed inline:
```mermaid
flowchart LR
A --> B
```
Avoid screenshots. Mermaid renders cleanly in dark and light, scales, and stays editable.
Admonitions
Use sparingly. They draw attention; over-use teaches readers to skip them.
```admonish tip
For when there's a non-obvious helpful shortcut.
Links
- Link to other docs on first mention of a concept:
[Marker protocol](../reference/markers.md). - Use repo links (
github.com/Pantani/sunscreen/...) sparingly — they go stale faster than internal links. Prefer internal docs when possible. - Never link to "click here" — always link the noun.
Headings
- One
H1per page (the title). H2for major sections.H3sparingly. If you needH4, the section probably wants splitting.
What not to write
- Don't describe the implementation of a command — describe the contract. Implementation belongs in code comments or ADRs.
- Don't include a "Conclusion" or "Summary" section. The page is the conclusion.
- Don't include a date or version-of-writing in the page body. Versions live in
CHANGELOG.md; the docs trackmain. - Don't apologize ("This is a quick guide…"). Just give the content.
When in doubt
Look at a sibling page in the same track. Match its rhythm.