Sharing Configuration Across a Marketplace of Photons
When you build a set of related photons that work together, they often need to agree on a few values: a data directory, an API base URL, a tenant ID, a sync interval. This guide covers how to do that today, the patterns to avoid, and the open enhancement that will eventually make it ergonomic.
What is a marketplace?
A marketplace is a directory of .photon.ts files marked by a .marketplace/ folder (containing a photons.json manifest). Photons in the same marketplace can call each other by name and are typically published, installed, and updated as a unit. See Marketplace Publishing for the full setup.
A real example: a "kith" marketplace might contain 28 photons, all reading from and writing to one shared ~/kith-data/ directory.
The trap to avoid
Every photon's first instinct, when it needs config, is to reach for process.env.MY_DATA_DIR. This works for one photon. It does not work for a marketplace, because:
- The user has to set the env var separately for every photon.
- The env var is invisible from inside Beam, the CLI, and the MCP UI. The user can change runtime behavior only by editing shell config and restarting the daemon.
- Each photon ends up with its own slightly-different copy of the path-resolution logic, and they drift.
If you find yourself writing process.env.SOMETHING inside a class extending Photon, that is the symptom. The cure is protected settings.
Pattern 1: each photon declares its own settings
The simplest shape. Every photon in the marketplace declares the same setting independently:
// kith-mail.photon.ts
export default class KithMail extends Photon {
/** User-tunable knobs */
protected settings = {
/** Directory where kith data lives */
dataDir: '~/kith-data',
};
}// kith-contacts.photon.ts
export default class KithContacts extends Photon {
/** User-tunable knobs */
protected settings = {
/** Directory where kith data lives */
dataDir: '~/kith-data',
};
}The user has to set dataDir once per photon (photon cli kith-mail settings --dataDir ..., then kith-contacts settings --dataDir ..., etc.). This is verbose, but it works on day one without any other moving parts.
Use this pattern when:
- The marketplace is small (2-3 photons).
- The shared knob has a sensible default that most users won't change.
Pattern 2: one config photon, others delegate
When the marketplace is bigger, designate one photon as the source of truth and have the rest call it.
// kith-config.photon.ts
export default class KithConfig extends Photon {
/** Marketplace-wide configuration. Other kith photons read these via this.call. */
protected settings = {
/** Root directory where all kith photons read and write */
dataDir: '~/kith-data',
/** Base URL for the upstream sync API */
apiBaseUrl: 'https://api.kith.example.com',
};
/** Read the shared data directory */
getDataDir() {
return { dataDir: this.settings.dataDir };
}
/** Read the shared API base URL */
getApiBaseUrl() {
return { apiBaseUrl: this.settings.apiBaseUrl };
}
}// kith-mail.photon.ts
/**
* @photon config ./kith-config.photon.ts
*/
export default class KithMail extends Photon {
constructor(private config: any) {} // injected from @photon config
async sync() {
const { dataDir } = await this.config.getDataDir();
// ...use dataDir
}
}The user changes dataDir once on kith-config and every photon that depends on it picks up the new value on the next call. The @photon tag declares the dependency so the daemon loads kith-config first; this.call (or the injected proxy) routes the request through the same daemon process.
Use this pattern when:
- More than three photons share the same knob.
- You want a single command to change a marketplace-wide setting.
- You want the shared values to appear in only one settings tool, not N.
Known gap: marketplace-level shared settings
There is no first-class @sharedSetting or marketplace.json settings block today. Pattern 2 is the manual equivalent. A future enhancement is tracked: declaring a setting at the marketplace level, with all member photons reading the same persisted value via this.settings, with no config-photon boilerplate.
If you hit a case where Pattern 2 is awkward (for example, you want every photon's read to be lock-free and synchronous), file an issue describing the use case so it can inform the design.
Cross-photon calls: known reliability constraint
Inside the same daemon, this.call('peer.method', args) and @photon-injected proxies route through the daemon's RPC bus. There has been a class of bugs where a photon calling a sibling in the same marketplace would fall back to a slow path or, in rare cases, end up with a stale instance. If you see strange behavior on cross-photon calls inside one marketplace:
- Check
~/.photon/.data/daemon.logfor routing warnings. - As a temporary workaround, photons can write to the shared filesystem layout directly (see Pattern 1 for setting
dataDir). This sidesteps the RPC path entirely. - File an issue with reproduction steps. The fix belongs in the runtime, not your photon.
The bypass is a workaround, not a recommended pattern. It splits write logic across photons and makes the data layout drift over time. Always prefer this.call once the underlying issue is resolved for your case.
Checklist for a multi-photon marketplace
When designing a set of related photons:
- [ ] Identify every value that should be runtime-configurable. Each goes on
protected settings, notprocess.env. - [ ] Decide which knobs are per-photon and which are marketplace-wide.
- [ ] If you have more than two marketplace-wide knobs, build a config photon (Pattern 2).
- [ ] Document the shared knobs in the marketplace's CLAUDE.md or README so future readers know where to set them.
- [ ] If your photons share a data directory, define the layout in one place (the config photon's docs or a top-level
LAYOUT.md) so every writer agrees.
