> ## Documentation Index
> Fetch the complete documentation index at: https://docs.photon.codes/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Use Stable documentation by default. Honor an explicit Beta request or a URL under /docs/beta/. If the requested version conflicts with the installed CLI package or API origin, clarify the target before writing integration code.
> Pages under /docs/beta/ document Beta; other product pages document Stable. Keep the CLI package, commands, API origin, and credentials within the selected version. State the documentation version in your answer.
> For MCP search, always pass version: Stable or version: Beta. Unfiltered search mixes both versions. For filesystem reads, keep Beta queries under /beta/ and exclude /beta/ from Stable queries; discover paths before reading them.
> The public docs base is https://photon.codes/docs. Convert MCP page paths to public URLs under that base, preserving /beta/ when present. Read https://photon.codes/docs/skill.md for version selection and https://photon.codes/docs/llms.txt for the version indexes.

# Run a local App

> Try registration, customer consent, credential exchange, and verified events with a generic Node.js backend.

The generic example in the [Photon docs repository](https://github.com/photon-hq/docs) lives in `examples/apps`. It needs Node.js 24 and uses the public `jose` and `standardwebhooks` packages. It does not depend on Convex or a Photon private package.

> [!WARNING]
>
> This is a local teaching example for a disposable test installation. Credentials, sessions, and deduplication records live only in memory and disappear on restart. It acknowledges events after accepting them into memory, not durable storage. Do not use it for production traffic or customer data.

## 1. Run an HTTPS tunnel

Forward an HTTPS URL to your local port 8787. For example, with ngrok installed and configured:

```bash theme={null}
ngrok http 8787
```

Keep it running and copy the HTTPS forwarding origin. If the URL changes, update both App registration URLs and the server's `PUBLIC_ORIGIN`.

## 2. Register a test App

Follow [Register an App](https://docs.photon.codes/docs/beta/apps/register.md), using:

| Setting | Value |
| - | - |
| HTTPS callback | `https://YOUR_TUNNEL/photon/callback` |
| Event webhook URL | `https://YOUR_TUNNEL/photon/webhooks` |
| Requested permissions | `organization:read`, `messages:read` |
| Subscribed events | `message.received` only; this example intentionally handles that event |

Save the App ID, key ID, downloaded PEM, and revealed webhook signing secret.

## 3. Configure and start your backend

From your checkout of the docs repository:

```bash theme={null}
cd examples/apps
npm ci
cp .env.example .env
```

Edit `.env` with your own editor. Do not paste its contents into a public issue or screen recording.

| Variable | Source |
| - | - |
| `PHOTON_API_ORIGIN` | Photon API origin for the enabled environment |
| `PHOTON_DASHBOARD_ORIGIN` | HTTPS Dashboard origin for that same environment |
| `PHOTON_TOKEN_URL` | Exact AuthKit token endpoint supplied by Photon for the environment; do not derive it from the API origin |
| `PUBLIC_ORIGIN` | Your tunnel's HTTPS origin, with no path |
| `PHOTON_APP_ID` | Registered App ID |
| `PHOTON_KEY_ID` | The key ID corresponding to your PEM |
| `PHOTON_PRIVATE_KEY_PATH` | Local path to the downloaded PEM, relative to `examples/apps` or absolute |
| `PHOTON_WEBHOOK_SECRET` | Revealed `whsec_…` secret, including its prefix |

The example requires HTTPS configuration. Use the target environment's hosted Dashboard for this walkthrough. Its `.env.example` intentionally contains placeholders so it cannot accidentally connect to production.

```bash theme={null}
npm test
npm start
```

Open **the public HTTPS tunnel URL**, not `http://localhost:8787`. The callback uses a secure session cookie bound to that public host.

## 4. Install as a customer

1. Click **Connect to Photon** on the example homepage.
2. In Photon Dashboard, sign in, select your test organization, review permissions, and choose project access.
3. Approve. Photon redirects to `/photon/callback` on your example server.
4. The backend validates state, redeems the code, stores the credentials in memory, obtains a token, and calls `/v1/organizations/current`.
5. The browser displays the installation ID, organization ID, and received-event count. It never receives client secrets or tokens.

A failure after credential delivery must be handled using the [redemption recovery rules](https://docs.photon.codes/docs/beta/apps/install.md#a-successful-http-response-may-not-contain-credentials). Do not repeatedly refresh the callback. If organization lookup alone failed, opening `/connection` retries that lookup using the already-stored credentials without redeeming again.

## 5. Receive an event

Use a messaging channel already connected to one of the installation's selected **Photon projects**. Send a test message into that channel. You do not install the App separately inside each project.

Photon sends `message.received` to `/photon/webhooks`. The backend verifies the signature, matches the installation and organization, checks the message project, and increments the count once per event. Open `/connection` to check the count; this deliberately simple example does not use polling or a WebSocket UI.

To add replies, follow [Send a reply through the API](https://docs.photon.codes/docs/beta/apps/webhooks.md#send-a-reply-through-the-api), request `project:write`, and have an existing installation approve the permission update. The example never sends messages automatically.

## Before adapting this into your product

* Replace the demo browser session with your product's authenticated customer session; enforce tenant ownership on every installation and operation.
* Persist installation credentials encrypted at rest and save them before further setup calls. Keep App-level keys in a secret manager.
* Replace the in-memory event set and counter with durable, atomic deduplication and a processing queue. Handle valid events that arrive before installation binding completes.
* Validate every subscribed event's schema and implement retries and lifecycle changes. Bound request size, sessions, queue growth, and rate limits for your service.
* Implement a user-visible disconnected/recovery flow. Restarting a production server must not require users to reinstall.
* Use a stable HTTPS domain. Remove tunnel and in-memory-demo assumptions.

These are backend responsibilities. Customers installing a hosted App should not handle PEM files, signing secrets, or environment variables.

## Troubleshooting

| Symptom | Check |
| - | - |
| Registration works but the installed-App list is empty | Registration and installation are separate. Start from your product's Connect button. |
| Callback rejected | Same browser/session, exact callback URL, unexpired state, and public HTTPS host. An old callback is not a reusable install link. |
| Assertion rejected | App ID, registered key ID and PEM, endpoint-specific audience, fresh `jti`, clock, and environment. |
| `created` was not returned by redemption | Follow recovery guidance; do not assume HTTP 200 includes credentials. |
| Token or organization lookup fails | Installation credentials, environment token endpoint, lifecycle state, and `organization:read`. |
| Webhook signature fails | App `whsec_…`, untouched raw body, all three headers, and server clock. Do not use the PEM to verify the HMAC. |
| No events | Subscription and permission approval, project selection, reachable tunnel, enabled App subscriptions in the environment, and actual new channel traffic. Registration does not backfill old messages. |
| `Installation binding pending` | Setup may still be resolving the organization, or this server has lost its in-memory credentials. Check `/connection`; recover a lost installation instead of guessing its tenant. |
| API 403 after selecting a project | The operation also needs the approved permission. Project discovery is not proof of access, and adding a project does not approve a pending permission update. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.