Skip to content

Project structure

Concept

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)
PathWhat it isTrigger
src/lexicons/com/example/hello.jsonA lexicon. Its id must equal its path: com.example.hello—
src/lexicons/com/example/hello.luaThe script for a query or procedure lexicon beside itxrpc.query:com.example.hello
src/lexicons/xyz/statusphere/status.create.luaA record script for a record lexicon beside it. create, update, delete, or index for all threerecord.create:xyz.statusphere.status
src/jobs/export.luaA job scriptjob.run:export
src/labels/<NSID>.lua, src/labels/_actor.luaA label script, for labels on records of that collection or on accountslabeler.apply:<NSID>
src/lib/format.lua, or any other pathA module, which scripts import—
anything.test.luaA 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 id doesn’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.lua beside a record lexicon (it has to say create, update, delete or index);
  • a subdirectory under src/jobs/ or src/labels/, which are flat;
  • a script in a language no installed interpreter runs.
[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
TableWhat it holdsOn a deploy
[happyview]The HappyView versionIgnored: 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 versionInstalled from happyview.lock
[lexicons."<NSID>"]Per-lexicon settings: backfill, target_collection, action, indexed, token_costApplied
[scripts."<trigger>"]Per-script settings: description, outbound_xrpcsApplied
[network_lexicons]include: NSIDs to track from the networkApplied
[labels]subscribe: labeler DIDs to subscribe toApplied
[dev]port, backfillIgnored, with a warning
[jetstream]protocol, compressionIgnored, with a warning
[modules."<name>"]A compiled Rust script moduleApplied

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.

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.

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.

  • Lexicons: add a record lexicon and start indexing.