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

🌱 Learn

New to Solana, Rust, or both? Start here. We assume zero prior knowledge.

Begin learning →

🛠 Guides

Task-oriented walkthroughs. "How do I scaffold a CRUD?" "How do I deploy to devnet?"

Browse guides →

📖 Reference

Every command, every flag, every exit code. For when you know what you want.

Open reference →


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-markers repairs safe drift.
  • Supervised dev loop. chain serve runs Surfpool (or solana-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 serve runs Surfpool (or solana-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:

TaskAnchor CLISunscreen
Create workspaceanchor initsunscreen chain new --framework anchor --frontend vite
Add instructionhand-edit lib.rssunscreen scaffold instruction Create --program app
Add CRUD slicemanual (~200 lines)sunscreen scaffold crud Post --program app
Watch + rebuild + regenerate clientsrun 3 terminalssunscreen chain serve
Diagnose toolchaintrial and errorsunscreen doctor

Anchor stays under the hood. Sunscreen does not replace it; it composes with it.

Next

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.

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:

  1. Open https://github.com/Pantani/sunscreen/releases/latest.
  2. Download the archive matching your platform (sunscreen-aarch64-apple-darwin.tar.xz, sunscreen-x86_64-unknown-linux-gnu.tar.xz, …).
  3. Extract and move sunscreen into 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 --force and 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

  • sunscreen installed (Installing).
  • anchor CLI on your PATH (anchor --version should 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)

Tip

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

## Step 2 — Look around
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 ... end markers that sunscreen can edit on later commands without breaking what you wrote in between.
  • Anchor.toml — standard Anchor config, points at the program.

Tip

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.

## Step 3 — Build
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:

  1. anchor build produced target/idl/my_app.json and the program's .so.
  2. 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 build is a thin orchestration over anchor build + Codama generation.
  • scaffold instruction mutated marked regions only, leaving your code untouched.

Next

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).
  • anchor CLI, solana CLI, Node 18+ with pnpm.
  • 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:

  1. Created an Anchor workspace with a Vite frontend.
  2. Scaffolded a Metaplex NFT recipe slice (mint, metadata, master edition).
  3. Added a sample frontend hook for minting (TanStack Query).

You'll see a summary table at the end listing files created.

Tip

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.

## Step 2 — Build
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

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

TypeWhat it is
u8 .. u64, i8 .. i64unsigned/signed integers
booltrue / false
Stringheap-allocated UTF-8 string
Vec<T>heap-allocated growable array of T
Pubkey32-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 (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: true if 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:

NetworkURLUse
localnethttp://127.0.0.1:8899a validator on your laptop (sunscreen's chain serve launches one)
devnethttps://api.devnet.solana.comfree SOL via faucet, public, test programs here first
testnethttps://api.testnet.solana.comvalidator testing, not for app dev
mainnethttps://api.mainnet-beta.solana.comproduction (Solana sometimes still calls this mainnet-beta in URLs)

The development flow you'll use

  1. Write the program (sunscreen scaffolds it).
  2. chain serve — runs against localnet with hot reload.
  3. sunscreen deploy devnet — push to devnet, test with real fees + real wallets.
  4. 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 init the same PDA twice, the second call fails. Use init_if_needed or 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

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

FilePurpose
programs/my_app/src/state/post.rsPost account struct with #[account]
programs/my_app/src/instructions/create_post.rscreate_post handler
programs/my_app/src/instructions/read_post.rsread_post handler
programs/my_app/src/instructions/update_post.rsupdate_post handler
programs/my_app/src/instructions/delete_post.rsdelete_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.tsTypeScript 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
FlagDefaultWhat it does
--program <name>requiredprogram to scaffold into
--fields "<spec>"title:string,body:stringcomma-separated name:type pairs for the account struct
--frontend-hookoffalso generate React Query hooks (requires frontend: react in sunscreen.yml)
--dry-runoffprint what would change without writing
--jsonoffmachine-readable summary
--forceoffoverwrite 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 syntaxAnchor type
name:stringString
name:boolbool
name:u64 (also u8, u16, u32, u128)matching unsigned int
name:i64 (also i8, i16, i32, i128)matching signed int
name:pubkeyPubkey
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

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:

  1. Debounces (waits ~200ms for related saves).
  2. Runs anchor build.
  3. If build succeeded and a frontend is configured, runs Codama to regenerate clients.
  4. Touches app/.sunscreen/reload so 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

  1. sunscreen chain serve — TUI appears, validator boots in ~2s.
  2. Open programs/my_app/src/instructions/create_post.rs, edit a handler.
  3. Save. Within ~3s: anchor builds, Codama regenerates app/src/clients/, your Vite dev server reloads.
  4. Test in browser. Fix bugs. Repeat.
  5. q to 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 serve requires 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 serve is for local dev only.

Going further

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 build succeeded).
  • solana CLI 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:

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

SymptomCauseFix
insufficient fundsnot enough SOLairdrop or use the web faucet
program too largebinary > buffer sizecheck --max-len on solana program deploy, or upgrade in chunks
transaction simulation failedprogram-side check failedrun the test suite locally first
BlockhashNotFoundRPC overloaded or clock skewretry; consider a private RPC (Helius, Triton)

Next

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

Warning

--final is irreversible. The program can never be upgraded again. Only do this for programs that you've audited and tested exhaustively.

## Step 6 — Update clients
sunscreen generate clients

Commit the regenerated clients. Your frontend now points at mainnet program IDs.

Common pitfalls

SymptomCauseFix
RPC error: 429 Too Many Requestspublic RPC throttleduse a private RPC via solana config set --url
BlockhashNotFound mid-deployRPC dropped youretry; Anchor's deploy is resumable
Wrong wallet usedenvironment variable leakedinspect solana config get before deploying
Forgot to update IDLclients have stale shapesunscreen generate clients and redeploy frontend

Going further

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

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: ract instead of react).
  • Missing required field for a chosen variant (e.g. framework: anchor requires a programs: 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> -- --help to 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?

CLI overview

sunscreen [GLOBAL_FLAGS] <COMMAND> [ARGS] [FLAGS]

Top-level commands

CommandWhat it does
chain newCreate a new workspace (Anchor or Pinocchio)
chain buildRun anchor build + Codama regeneration
chain serveSupervised dev loop with local validator
chain doctorDiagnose toolchain + workspace markers
scaffoldAdd instruction, account, event, error, program, or recipe
generateGenerate IDL, Codama clients, frontend hooks
appManage plugins (install, list, run, hook, marketplace)
doctorDetect installed toolchain versions
initInteractive wizard for new users
examplesBrowse embedded example projects
quickstartComposite recipe shortcuts (token, nft, dao, blog)
walletWallet helpers (new, airdrop)
deployDeploy programs to a network
learnOpen a topic in the embedded learn index

Global flags

FlagWhat it does
--jsonmachine-readable output on stdout; human messages on stderr
-v / -vv / -vvvverbosity (warn / info / debug)
--workdir <DIR>override working directory
--config <FILE>path to an alternative sunscreen.yml
--helpper-command help
--versionprint sunscreen version

Exit codes

CodeMeaning
0success
1unexpected error (bug) — please report
2toolchain missing (anchor, solana, cargo, pnpm, …)
3invalid config (sunscreen.yml schema violation)
4user input conflict (resource exists, ambiguous flag, …)
5missing workspace (no sunscreen.yml upward from cwd)
9plugin 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, and error.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).

FlagDefaultDescription
--framework <name>anchoranchor or pinocchio
--frontend <name>nonenone, vite, next
--path <DIR>./<NAME>output directory
--dry-runoffprint 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.

FlagDefaultDescription
--headlessoffNDJSON events on stdout suitable for CI logs
--no-codamaoffskip 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.

FlagDefaultDescription
--headlessoffNDJSON stream, no TUI
--no-codamaoffskip Codama on rebuild
--no-frontendoffskip frontend reload notifications
--runtime <name>from sunscreen.ymlsurfpool 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>150watcher 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.

FlagDefaultDescription
--fix-markersoffreconstruct 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

FlagDefaultDescription
--program <name>required for mostwhich program to scaffold into
--dry-runoffprint planned diffs without writing
--jsonoffmachine-readable summary
--forceoffoverwrite 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:

NounWhat 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

CodeWhen
0success
2toolchain missing (rare; only for recipes that compile-check)
3invalid config
4symbol already exists, ambiguous flag, or recipe preflight failure
5not in a workspace
9plugin 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.

FlagDefaultDescription
--program <NAME>first IDLprogram 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.

FlagDefaultDescription
--program <NAME>all built IDLs in target/idlprogram to export
--out-dir <DIR>clients/idloutput 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.

FlagDefaultDescription
--program <NAME>all built IDLsprogram to generate hooks for
--frontend-path <DIR>from sunscreen.ymlrequired when the workspace was scaffolded with --frontend none
--target <NAME>from project configreact, 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

CodeWhen
0success
2pnpm or another required dependency missing
3sunscreen.yml invalid
5not in a workspace

Tips

  • chain build calls generate clients automatically. Run generate directly 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 / flagDescription
<SOURCE>local path (./plugins/foo) or git URL (github.com/org/foo.git)
--versionsemver string; with or without v prefix
--dry-runprint 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-2022
  • sunscreen-apps/yellowstone-indexer

This is a static, embedded list. Remote registry support is planned.

Exit codes

CodeWhen
0success
3sunscreen.yml invalid
4name conflict, missing source, version not semver
5not in a workspace
9plugin 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

ToolDetected via
rustcrustc --version
cargocargo --version
anchoranchor --version
solanasolana --version
cargo-build-sbfcargo build-sbf --help
pnpmpnpm --version
nodenode --version
codamafrom local node_modules/.bin/codama
surfpoolsurfpool --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

CodeWhen
0always — 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.

FlagDefaultDescription
--non-interactiveoffdisable prompts and require flag-based input
--from-preset <NAME>nonepreset to apply when no prompts are available
--frontend <name>vitenone, vite, next
--path <DIR>./<NAME>output directory
--dry-runoffprint planned files without writing

examples

sunscreen examples <SUBCOMMAND>
SubcommandWhat 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".

RecipeWhat it builds
tokenAnchor workspace + SPL Token recipe
nftAnchor workspace + Metaplex NFT recipe
daoAnchor workspace + DAO voting scaffolds
blogAnchor workspace + CRUD Post resource
FlagDefaultDescription
--name <NAME>promptedproject name (required in --non-interactive)
--cluster <NAME>localnetlocalnet, devnet, mainnet — used for the generated next steps
--non-interactiveoffdisable prompts
--frontend <name>vitenone, vite, next
--path <DIR>./<NAME>output directory
--dry-runoffprint planned operations without writing

wallet

sunscreen wallet <SUBCOMMAND>
SubcommandWhat it does
new [<NAME>] [--out <FILE>] [--no-bip39-passphrase] [--dry-run]Generate a keypair. When --out is omitted, lands under .sunscreen/wallets/<NAME>.json
listList 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 / flagDefaultDescription
<TARGET>requiredlocalnet, devnet, or mainnet (positional, value-enum)
--program <NAME>all programspass through to Anchor for a single program
--verifyoffrun anchor verify after deploy when supported
--yes-i-understand-costoffrequired for mainnet
--dry-runoffprint 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.

Warning

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.

## Top-level shape
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

KeyTypeRequiredDescription
versionintyesschema version; sunscreen migrates older values automatically
projectobjectnoname, framework (anchor/pinocchio), frontend (none/vite/next)
toolchainobjectnoexternal tool version pins
scaffoldingobjectnoscaffolder/marker behaviour knobs
programsarraynoone entry per program in the workspace
workspaceobjectnoworkspace-level layout
clustersobjectnoper-cluster RPC + wallet (localnet/devnet/mainnet)
runtimeobjectnolocal dev runtime preferences
pluginsarraynodeclared plugins

programs[]

FieldTypeRequiredDescription
namestringyessnake_case program name
pathpathyesrelative to workspace root
program_idstringnofilled by deploy; null until first deploy

clusters.<name>

FieldTypeRequiredDescription
urlstringyesRPC endpoint
walletstringyespath 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

FieldTypeDefaultDescription
engineenumsurfpoolsurfpool or test-validator (sunscreen falls back from Surfpool to test-validator when Surfpool isn't on PATH)
portint8899validator RPC port
faucet_solint100seed balance for the local runtime faucet

plugins[]

FieldTypeRequiredDescription
sourcestringyeslocal path or git URL
versionsemveryeswith 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

RecipeWhat it generates
CRUDAn account + 4 instructions (create/read/update/delete) + 3 events + 2 errors + TS test
SPL TokenAn SPL Token mint + transfer slice
Metaplex NFTA 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 4 and 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-hook regenerates 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

FlagDefaultDescription
--program <name>requiredwhich program to scaffold into
--fields <spec>title:string,body:stringaccount fields
--frontend-hookoffalso generate matching React/Solid hooks
--dry-runoffprint plan only
--jsonoffmachine-readable summary
--forceoffoverwrite marker content even if a symbol exists

Field types

--fields accepts comma-separated name:type pairs:

SpecAnchor typeNotes
name:stringStringUTF-8, max 256 bytes by default
name:boolbool
name:u8name:u128u8u128
name:i8name:i128i8i128
name:pubkeyPubkey
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:

PathStatus
programs/blog/src/state/post.rscreated
programs/blog/src/state/mod.rspatched (marker)
programs/blog/src/instructions/create_post.rscreated
programs/blog/src/instructions/read_post.rscreated
programs/blog/src/instructions/update_post.rscreated
programs/blog/src/instructions/delete_post.rscreated
programs/blog/src/instructions/mod.rspatched
programs/blog/src/lib.rspatched (dispatch markers)
programs/blog/src/events.rspatched (3 new variants)
programs/blog/src/errors.rspatched (2 new variants)
tests/post.spec.tscreated

Generated instructions

InstructionAccountsEffect
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 the authority on 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

CodeWhen
0success
4preflight conflict (symbol already exists; pass --force to overwrite marker content)
5not 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

FlagDefaultDescription
--program <name>requiredwhich program to scaffold into
--decimals <n>9mint decimals
--frontend-hookoffgenerate matching React/Solid hook
--dry-runoffprint plan only
--jsonoffsummary on stdout
--forceoffoverwrite marker content even on conflict

Generated files

For scaffold spl-token MyToken --program app:

PathStatus
programs/app/src/state/my_token.rscreated (mint metadata account)
programs/app/src/instructions/init_my_token.rscreated
programs/app/src/instructions/mint_my_token.rscreated
programs/app/src/instructions/transfer_my_token.rscreated
programs/app/src/instructions/mod.rspatched
programs/app/src/lib.rspatched (dispatch)
programs/app/src/events.rspatched
programs/app/src/errors.rspatched
tests/my_token.spec.tscreated

Generated instructions

InstructionEffect
init_my_tokenCreates the mint PDA, sets authority and decimals
mint_my_tokenMints amount to a destination token account (CPI to spl-token)
transfer_my_tokenTransfers amount between token accounts

Generated events

  • TokenInitialized { mint, authority, decimals }
  • TokenMinted { mint, recipient, amount }
  • TokenTransferred { from, to, amount }

Generated errors

  • MintUnauthorized
  • InsufficientBalance

Notes

  • This recipe uses the classic SPL Token program (Tokenkeg…). For SPL Token-2022, install the sunscreen-apps/spl-token-2022 plugin and use sunscreen 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

CodeWhen
0success
4preflight conflict
5not in a workspace

Metaplex NFT recipe

sunscreen scaffold metaplex-nft <NAME> --program <PROGRAM> [FLAGS]

Generates a Metaplex Token Metadata-compatible NFT mint slice.

Flags

FlagDefaultDescription
--program <name>requiredwhich program to scaffold into
--collection <name>noneoptional collection account name
--frontend-hookoffgenerate matching React/Solid hook
--dry-runoffprint plan only
--jsonoffsummary on stdout
--forceoffoverwrite marker content even on conflict

Generated files

For scaffold metaplex-nft MyNft --program app:

PathStatus
programs/app/src/state/my_nft.rscreated
programs/app/src/instructions/mint_my_nft.rscreated
programs/app/src/instructions/mod.rspatched
programs/app/src/lib.rspatched (dispatch)
programs/app/src/events.rspatched
programs/app/src/errors.rspatched
tests/my_nft.spec.tscreated

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:

  1. Creates a mint with 0 decimals (NFT convention).
  2. Mints 1 token to the authority's associated token account.
  3. CPI to Metaplex Token Metadata to create metadata + master_edition.
  4. Emits NftMinted { mint, owner, uri }.

Generated events

  • NftMinted { mint, owner, uri }

Generated errors

  • MetadataUriTooLong
  • MintAuthorityMismatch

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.toml gets mpl-token-metadata added as a dep on first scaffold.
  • --collection wires the minted NFT into a Metaplex collection account.

Exit codes

CodeWhen
0success
4preflight conflict
5not 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:

CapabilityDefaultToggle
Read workspace filesallowedalways on
Write workspace filesdeniedpermissions.workspace_write = true
Read outside workspacedenieddeclare each path in permissions.declared_paths
Network accessdeniedpermissions.network = true (user-approved at install)
Spawn subprocessesdeniednot 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.

EventWhenFields
build_startpipeline startsframework, programs[]
build_progressstep transitionstep (anchor_build / cargo_build_sbf / codama)
build_okpipeline succeededprograms[], duration_ms
build_failpipeline failedstep, exit_code, stderr_tail

Codama events

EventWhenFields
codama_startCodama invocation beginsfrontend
codama_okclients writtenfiles_written (int), duration_ms
codama_failCodama failedexit_code, stderr_tail

Frontend notify

EventWhenFields
frontend_notifiedreload file touchedpath

Serve / watcher events

Emitted by chain serve --headless.

EventWhenFields
serve_startserver readyrpc_url, ws_url, validator
validator_logvalidator wrote a lineline (string)
watcher_batchfile changes debouncedpaths[] (relative), kind (save/delete)
pipeline_triggeredwatcher kicked a buildpaths[]
serve_stopshutdown completereason (ctrl_c/error)

Toolchain events

Emitted by any command that reaches a missing toolchain.

EventFields
toolchain_missingtool, 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.
  • event is 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

ExitCodeMeaning
0success
1unexpecteda bug — please report with RUST_BACKTRACE=1 output
2toolchain_missinga required external tool (anchor, solana, …) was not found
3invalid_configsunscreen.yml failed schema validation
4user_inputconflicting / ambiguous / unrecognized user request
5missing_workspaceno sunscreen.yml found upward from cwd
9plugin_runtimea 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)

codenext_step
toolchain_missing.anchorInstall via AVM: cargo install --git https://github.com/coral-xyz/anchor avm --locked && avm install latest
toolchain_missing.solanash -c "$(curl -sSfL https://release.solana.com/stable/install)"
toolchain_missing.pnpmnpm i -g pnpm
toolchain_missing.cargo_build_sbfComes with the solana install — re-run the solana installer

Invalid config (exit 3)

codeTrigger
invalid_config.schemaTop-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_unknownYAML declares a version newer than this binary supports

User input (exit 4)

codeTrigger
user_input.path_conflictTarget directory exists (sunscreen refuses to overwrite without --force)
user_input.symbol_existsA scaffold target (instruction, account, …) is already defined
user_input.recipe_preflightA recipe's preflight detected conflicts
user_input.ambiguous_flagTwo flags conflict (e.g. --dry-run + --force)
user_input.semver--version argument isn't valid semver
network.rate_limitedFaucet returned 429
network.unreachableRPC didn't respond

Missing workspace (exit 5)

codeTrigger
missing_workspaceNo sunscreen.yml in cwd or ancestors

Plugin runtime (exit 9)

codeTrigger
plugin_runtime.protocolPlugin emitted invalid JSON-RPC
plugin_runtime.sandboxPlugin tried to write outside workspace or open a forbidden network connection
plugin_runtime.crashPlugin exited unexpectedly (non-zero, no response)
plugin_runtime.manifestPlugin 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:

  1. Create a new file (foo.rs).
  2. Edit instructions/mod.rs to re-export from it.
  3. Edit lib.rs to 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

TagLives inContent
dispatchlib.rspub fn <ix>(...) -> Result<()> { ... } wrappers
instructionsinstructions/mod.rspub use <module>::*; re-exports
statestate/mod.rspub mod <account>; declarations
eventsevents.rspub use <event>::*; re-exports
errorserrors.rspub use <error>::*; re-exports
error_variantsinside an error enumenum 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:

SiteRepair behaviour
instructions/mod.rs instructions marker missingreconstruct, listing files present in instructions/
lib.rs dispatch marker missingreconstruct only if all generated instruction files define pub fn handler (else refuse)
errors.rs error_variants marker missingreconstruct 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

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) — notify events, debounced, dedup'd, filtered.
  • Validator adapters (surfpool.rs, testvalidator.rs) — abstracted behind a LocalValidator trait.
  • Supervisor (supervisor.rs, serve.rs) — long-running orchestrator for chain 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 minijinja rendering. 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

ConcernWhere
Config (sunscreen.yml)src/config/ — schema, loader, migrations
Toolchain detectionsrc/toolchain/ — uniform ToolReport for every external tool
Errorssrc/error.rs — single Error enum, code + next_step
TUI (chain serve)src/tui/serve_model.rs

Design principles

  1. Determinism. Same inputs → same outputs. Golden tests cover scaffolds; round-trip tests cover marker patches.
  2. 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.
  3. Two-layer plugin model. JSON-RPC over stdio is the floor; gRPC is the ceiling. Both share the same logical contract.
  4. CLI is the API. --json output is part of stability. Editor integrations don't need an SDK.

See also

Workspace model

A sunscreen workspace is a directory containing four things:

  1. A sunscreen.yml at the root.
  2. A Rust workspace Cargo.toml.
  3. An Anchor.toml (for Anchor) or nothing extra (for Pinocchio).
  4. 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

  1. Idempotency. Re-running with the same args is a no-op. Sunscreen detects "this symbol already exists in the marker" and exits 4 unless --force is passed.
  2. Marker isolation. Content outside markers is never touched. Content inside markers may be fully regenerated on every run.
  3. Drift detection. chain doctor finds missing or duplicated markers and reports them. chain doctor --fix-markers repairs 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:

FileMarker tag
programs/*/src/lib.rsdispatch
programs/*/src/instructions/mod.rsinstructions
programs/*/src/state/mod.rsstate
programs/*/src/events.rsevents
programs/*/src/errors.rserrors
Any error enum inside errors.rserror_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

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. Produces target/idl/<program>.json and target/deploy/<program>.so per 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-codama to 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:

CapabilityDefaultOpt-in mechanism
Read inside workspaceallowedalways on
Write inside workspacedeniedpermissions.workspace_write = true in manifest
Read outside workspacedenieddeclare each path in permissions.declared_paths[]
Networkdeniedpermissions.network = true + user approval at install
Subprocess spawndeniednot 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 — adds scaffold spl-token-2022 <Name> with the 2022-extension features.
  • yellowstone-indexer — adds scaffold 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 --flag instead.
  • Your team can just copy-paste a snippet (don't over-engineer).

See also

Anchor vs Pinocchio

Sunscreen supports two Solana program frameworks. Choose based on what you're optimizing for.

TL;DR

AnchorPinocchio
Mental modelHigh-level, macros do the heavy liftingLow-level, you wire account checks yourself
Compile-time guaranteesStrong (#[derive(Accounts)] checks at build time)Minimal (you assert at runtime)
Compute unitsHigher overheadNear-bare-metal
Code volumeLess code per instructionMore code per instruction
Sunscreen supportFull (scaffold, codegen, recipes, plugins)Workspace bootstrap + build only
When to pickNew projects, productivity-firstPerformance-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

FeatureAnchorPinocchio
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:

  1. Keep the Anchor program as the user-facing surface.
  2. Extract the hot instruction into a separate Pinocchio program.
  3. Have the Anchor program CPI into the Pinocchio program.

See also

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

PhaseStatus
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-dist release pipeline (Linux + macOS).
  • ✅ documentation site (this site).
  • ⏳ shell completions (bash, zsh, fish, pwsh).
  • ⏳ Homebrew tap.
  • ⏳ Windows artifact (cargo-dist matrix).
  • cargo dist plan CI verification.
  • cargo-binstall support.

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

IDTitleStatus
ADR-0001The sunscreen CLI: scope, vision, principlesAccepted
ADR-0002CLI design conventions (exit codes, flags, JSON contract)Accepted
ADR-0003Documentation strategyAccepted
ADR-0004Incremental scaffolding (markers)Accepted
ADR-0005Beginner onboarding surfaceAccepted
ADR-0006Pinocchio bootstrapAccepted

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.toml if 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

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 run anchor 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.

Warning

For when something can damage state or burn money.

Danger

For irreversible operations.

  • 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 H1 per page (the title).
  • H2 for major sections.
  • H3 sparingly. If you need H4, 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 track main.
  • 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.