Project structure
Read first: getting-started/running-locally
A project is a directory with a happyview.toml at its root and your code under src/. HappyView decides what each file is from its path alone, so there’s nothing to register: put a file in the right place and it’s part of your app.
my-app/├── happyview.toml settings, interpreters and libraries├── happyview.lock exact plugin versions (written by hpy)├── .env values for ctx.env while you develop├── src/│ ├── lexicons/ lexicons, and the scripts beside them│ ├── jobs/ job scripts│ ├── labels/ label scripts│ └── lib/ modules your scripts import (any name works)└── .happyview/ local data and downloads (not committed)src/: what a path means
Section titled “src/: what a path means”| Path | What it is | Trigger |
|---|---|---|
src/lexicons/com/example/hello.json | A lexicon. Its id must equal its path: com.example.hello | — |
src/lexicons/com/example/hello.lua | The script for a query or procedure lexicon beside it | xrpc.query:com.example.hello |
src/lexicons/xyz/statusphere/status.create.lua | A record script for a record lexicon beside it. create, update, delete, or index for all three | record.create:xyz.statusphere.status |
src/jobs/export.lua | A job script | job.run:export |
src/labels/<NSID>.lua, src/labels/_actor.lua | A label script, for labels on records of that collection or on accounts | labeler.apply:<NSID> |
src/lib/format.lua, or any other path | A module, which scripts import | — |
anything.test.lua | A test, wherever it is | — |
The extension picks the language: .lua, .js or .ts, as long as an interpreter for it is installed. Script types and triggers has every rule.
The build refuses a project it can’t make sense of, and says which file and why. The usual causes:
- a lexicon whose
iddoesn’t match its path, or two lexicons whose ids differ only in case; - a record script with no record lexicon beside it, or a bare
status.luabeside a record lexicon (it has to saycreate,update,deleteorindex); - a subdirectory under
src/jobs/orsrc/labels/, which are flat; - a script in a language no installed interpreter runs.
happyview.toml
Section titled “happyview.toml”[happyview]version = "3.0.0-alpha.42" # the HappyView version hpy runs
[instance]app_name = "statusboard"superadmin = "did:plc:…" # given the superadmin privilege at boot
[interpreters]lua = { did = "did:plc:mo7vq4jbioxwafy5zkcc3eux", name = "happyview-lua", channel = "alpha" }
[libraries]
[dev]backfill = false # only for hpy happyview dev| Table | What it holds | On a deploy |
|---|---|---|
[happyview] | The HappyView version | Ignored: an instance keeps its own version |
[instance] | app_name, and superadmin (a DID) | Ignored, with a warning if it differs |
[interpreters], [libraries] | Each plugin’s publisher did, name, and a channel or exact version | Installed from happyview.lock |
[lexicons."<NSID>"] | Per-lexicon settings: backfill, target_collection, action, indexed, token_cost | Applied |
[scripts."<trigger>"] | Per-script settings: description, outbound_xrpcs | Applied |
[network_lexicons] | include: NSIDs to track from the network | Applied |
[labels] | subscribe: labeler DIDs to subscribe to | Applied |
[dev] | port, backfill | Ignored, with a warning |
[jetstream] | protocol, compression | Ignored, with a warning |
[modules."<name>"] | A compiled Rust script module | Applied |
An unknown key anywhere is an error, as is a [lexicons."<NSID>"] that names no lexicon file.
Don’t edit [interpreters] and [libraries] by hand: hpy happyview add changes them and happyview.lock together. See Plugins and libraries.
happyview.lock
Section titled “happyview.lock”hpy writes it, and you commit it. It pins the exact version and content hash of every plugin, so your machine, CI and your instance all run the same ones. dev, check and test update it when it no longer matches happyview.toml. With --locked they fail instead, which is what CI wants. A deploy refuses a lock that doesn’t match.
While you develop, scripts read .env as ctx.env, so keep API keys and other secrets there, not in your scripts. create writes a .env.example to copy from, and .gitignore keeps .env out of git.
.env isn’t deployed. On an instance, ctx.env comes from the dashboard’s System > ENV Variables page. Test runs read .env.test instead.
.happyview/
Section titled “.happyview/”Local state hpy and the local instance keep: the SQLite database, the plugins it loaded, generated secrets, built modules and editor types. It’s in .gitignore. Delete it to start your local instance over.
Next steps
Section titled “Next steps”- Lexicons: add a record lexicon and start indexing.

