Skip to content

PHOTON_DIR and Namespace

Status: Canonical. Any code that disagrees is a bug. Date: 2026-04-17

This document defines how a photon's source file location determines where its data lives. There is one rule, and it is mechanical.


The contract

1. PHOTON_DIR is the outer boundary

A photon directory is a self-contained home for both source files and data. Once PHOTON_DIR is resolved, every subsystem (loader, memory, state, logs, schedules, cache, env, config) stores its data under ${PHOTON_DIR}/.data/. Nothing ever silently falls back to ~/.photon once a different PHOTON_DIR has been chosen.

~/.photon is simply the default PHOTON_DIR when nothing else is specified. It is not privileged.

2. Resolution of PHOTON_DIR

Priority, highest first:

  1. Explicit process.env.PHOTON_DIR.
  2. cwd if it qualifies as a photon directory (see §5).
  3. ~/.photon as the default.

Once resolved at process entry, PHOTON_DIR is frozen for the lifetime of the process and exported back to the environment so every child/subsystem sees the same value.

3. Namespace comes from directory position. Nothing else.

namespace = path.dirname(photonFile) relative to PHOTON_DIR
         = "" (empty) if the file is flat at the root of PHOTON_DIR
         = the subdirectory path otherwise

Examples:

Source fileNamespaceData directory
${PHOTON_DIR}/foo.photon.ts""${PHOTON_DIR}/.data/foo/
${PHOTON_DIR}/alice/foo.photon.tsalice${PHOTON_DIR}/.data/alice/foo/
${PHOTON_DIR}/payments/stripe.photon.tspayments${PHOTON_DIR}/.data/payments/stripe/
${PHOTON_DIR}/org/team/foo.photon.tsorg/team${PHOTON_DIR}/.data/org/team/foo/

The runtime never consults git, never consults remote URLs, never synthesizes a namespace. Namespace is a pure function of the file path relative to PHOTON_DIR.

4. Data layout mirrors source layout

If the source is flat at the root, data is flat at the root of .data/. If the source is organized into subdirectories, data is organized the same way. The two trees are always isomorphic. This is the only rule you need to predict where a photon's data lives.

5. What qualifies as a photon directory

A directory is a photon directory if it contains one or more .photon.ts files at any depth. When PHOTON_DIR is not set explicitly, the runtime checks whether cwd qualifies. If yes, cwd becomes PHOTON_DIR and process.env.PHOTON_DIR is set so downstream subsystems see it.

6. ~/.photon as the global default

~/.photon is the fallback PHOTON_DIR, no more and no less. Photons at its root are not global, they are just photons that happen to live in the default location. Installing a photon from another author places its file at ~/.photon/<author>/<name>.photon.ts, which produces the namespace <author> by rule §3 without any special casing.


What this replaces

The runtime previously derived a namespace for flat files by running git remote get-url origin on the base directory. That produced two failure modes:

  • Adding a git remote to a PHOTON_DIR flipped the namespace, orphaning existing data under the old namespace bucket.
  • Data could end up under a different namespace than the source file's directory, breaking the "data mirrors source" invariant.

The new rule eliminates both by construction. Git state of a directory has no semantic meaning to the runtime.


Migration from the old model

For installations that already have data stranded under a git-remote-derived namespace (e.g. .data/<owner>/<photon>/ where the source sits flat at the root), the runtime ships a one-release compatibility shim that moves such data into the correct canonical location on first access. The shim is scheduled for removal in the following minor release. After removal, a small photon migrate CLI may be offered to help users who missed the transition.


Implementation notes

  • All path resolution goes through photon-core/src/data-paths.ts. getDataRoot(baseDir) returns ${baseDir}/.data/. Callers that pass no baseDir get the resolved PHOTON_DIR (via getBase), which is correct by §1.
  • detectNamespace(dir) (git-remote based) is removed. It has no valid caller under this model.
  • PhotonLoader.resolveNamespace(absolutePath) computes namespace purely from path.relative(this.baseDir, absolutePath). No branch consults git or treats ~/.photon specially.
  • context.ts produces a PhotonContext whose dataDir is getDataRoot(baseDir), not hardcoded to HOME_PHOTON_DIR. This was the single line that silently broke Option B for every consumer that read context.dataDir.

Testing the contract

A runtime change passes the Option B compliance check if, for any PHOTON_DIR:

  1. Creating a new photon file writes source exactly where you put it, and data at the mirrored position under .data/.
  2. No data paths resolve under ~/.photon unless PHOTON_DIR itself resolves to ~/.photon.
  3. Changing the git state of the PHOTON_DIR (adding, removing, or changing a remote) produces no change in any data path.
  4. Two PHOTON_DIR trees operated on in sequence (or in parallel) do not share or leak any data between them.

Any failure of (1)-(4) indicates a subsystem that has not been routed through getDataRoot(baseDir) correctly.


8. Daemon central, data distributed

The runtime has exactly one piece of global infrastructure: the daemon. Everything else that looks like it belongs to a photon (state, memory, logs, env, schedules, config, instance data) is data and lives with the PHOTON_DIR that owns it.

Classification

ConcernClassificationLocation
Daemon socket / pid / logInfrastructure~/.photon/.data/daemon.* (one per user)
Webhook HTTP server (port, routing table)InfrastructureIn-process on the daemon
In-memory locks, channel buffers, session managersInfrastructure (transient)Daemon process memory
Bases registryInfrastructure~/.photon/.data/.bases.json
Scheduled jobsData{PHOTON_DIR}/.data/{ns}/{photon}/schedules/
Instance stateData{PHOTON_DIR}/.data/{ns}/{photon}/state/{instance}/
Memory (this.memory)Data{PHOTON_DIR}/.data/{ns}/{photon}/memory/
LogsData{PHOTON_DIR}/.data/{ns}/{photon}/logs/
Env / context / configData{PHOTON_DIR}/.data/{ns}/{photon}/...

The daemon is the mechanism that fires schedules, routes webhooks, serves MCP requests. It does not own the records — records live with their PHOTON_DIR.

Rule

A subsystem is infrastructure if it exists once per user. Everything else is data, and its location is a function of PHOTON_DIR.

Concretely: if you're considering where to store something, ask "does it make sense to have more than one of these, scoped per project/marketplace?" If yes, it's data. If no (there's only ever one daemon, one webhook port, one socket), it's infrastructure.

How the daemon handles data distributed across PHOTON_DIRs

One daemon serves all the PHOTON_DIRs the user has ever launched photons from. For that to work for long-lived concerns like schedules, the daemon maintains a bases registry at ~/.photon/.data/.bases.json:

json
{
  "bases": [
    { "path": "/Users/arul/Projects/kith", "firstSeen": "2026-04-17T09:00:00Z", "lastSeen": "2026-04-17T11:30:00Z" },
    { "path": "/Users/arul/.photon",       "firstSeen": "2026-04-10T12:00:00Z", "lastSeen": "2026-04-17T11:32:00Z" }
  ]
}

Every time the daemon handles an invocation from a PHOTON_DIR, it upserts that base with a fresh lastSeen. On startup, the daemon:

  1. Reads the registry.
  2. Prunes entries whose path no longer exists on disk (ENOENT). Entries with temporary read errors (e.g. unmounted network drive) are kept.
  3. For each surviving base, scans {base}/.data/*/*/schedules/*.json and reinstates the timers.

Schedule records themselves carry the originating baseDir inline so the daemon knows which context to re-enter when firing.

What this replaces

The legacy behavior wrote all schedules to ~/.photon/schedules/ regardless of which PHOTON_DIR spawned them (optionally redirected by PHOTON_SCHEDULES_DIR). That directly violated the Option B contract. The registry + per-base schedule layout restores it while keeping the daemon topology unchanged.

Released under the MIT License.