1# Development
2
3## First checkout
4
5Install `vp` using the [root README](../../README.md#install-vp). The checkout requires Node 24;
6Bun is optional. From the repository root:
7
8```sh
9vp i
10vp run dev
11```
12
13Open the pairing URL printed by the dev runner. The bare origin does not authenticate
14a new browser.
15
16Prefer a container? See [Dev container](../internals/devcontainer.md) for VS Code and Codespaces setup.
17
18## Choosing a dev process
19
20Use `vp run dev` for server and web, or `vp run dev:desktop` for the Electron client.
21`dev:server` and `dev:web` start those processes separately.
22See the [mobile README](../../apps/mobile/README.md) for native builds and Metro.
23
24Flags go directly after the task name, for example `vp run dev --home-dir /tmp/t3code-dev`.
25Add `--browser` to open a browser automatically.
26
27### State and ports
28
29Linked worktrees default to their own `.t3/userdata`, even when `T3CODE_HOME` is set.
30The main checkout defaults to `~/.t3/dev/userdata`. An explicit `--home-dir` wins in both cases.
31Never run a development server against the live `~/.t3/userdata`.
32See [test data](../../AGENTS.md#test-data) for copying a consistent database snapshot.
33
34Read ports from the `[dev-runner]` output. Worktrees derive stable preferences from their paths,
35but occupied ports can shift them. `T3CODE_PORT_OFFSET` or `T3CODE_DEV_INSTANCE` can select a
36different preference when needed.
37
38### Sharing and remote debugging
39
40`vp run dev --share` publishes the web port over the machine's tailnet and prints a pairing URL
41for that origin. Give the tester the complete URL, including its token. The dev runner removes
42its mapping on exit.
43
44Leave `VITE_HTTP_URL` and `VITE_WS_URL` unset. Vite proxies the backend through the browser's
45origin so the same build works over localhost and remote connections.
46
47Shared runs enable bundled dev to avoid a network round trip for each import level.
48`T3CODE_BUNDLED_DEV=0` opts out when debugging bundler differences. Two reload traps matter
49when changing this setup:
50
51- The web entry must dynamically import the app so React refresh initializes before application
52 chunks. Static imports can work on first load and fail after a route split.
53- Bundled dev rebuilds Tailwind through watched files. Its ordinary Vite hot-update hook expects
54 a server/module graph that Rolldown does not provide.
55
56The workarounds live in the [web entry](../../apps/web/src/bootstrap.ts) and
57[Tailwind plugin](../../apps/web/vite/tailwind.ts).
58
59#### Reusable dev credential
60
61Use this only on a hostname where you trust every service. Browsers send cookies to all ports
62on that hostname. Any service you visit there can receive the reusable admin credential,
63including services unrelated to T3 Code. If you run untrusted services on that hostname, keep
64normal per-environment pairing instead.
65
66To use one browser profile across web dev worktrees on the same hostname, generate one fixed
67value once:
68
69```sh
70openssl rand -hex 32
71```
72
73Put that value in the main checkout's gitignored `.env`:
74
75```dotenv
76T3CODE_DEV_AUTH_TOKEN=<the value generated above>
77```
78
79The `t3.json` Setup Worktree action links that file to each worktree's `.env`. The dev runner reads repository env files at startup. `.env.local` and inherited process
80environment values override `.env`, so no per-worktree export is needed after setup.
81
82For a manual worktree or launcher without that link, export the same fixed value instead:
83
84```sh
85export T3CODE_DEV_AUTH_TOKEN="<the value generated above>"
86```
87
88Do not generate a new value at startup. Start or restart `vp run dev --share` after configuration,
89then open its printed startup pairing URL once per browser profile on that hostname. Later web dev
90servers on the same hostname accept the shared cookie across ports. The cookie expires after 30
91days. Reload an old tab if its URL now serves a replacement environment.
92
93The token and startup pairing URLs are reusable administrative secrets. Never put them in a
94commit, pull request, or public output. Every server still seeds its own auth database record at
95startup and keeps its own SQLite data, signing key, and revocation state. Desktop and non-dev
96servers ignore the value. See [environment authentication](../internals/environment-auth.md#reusable-dev-credential)
97for the security model.
98
99## Checks
100
101Run checks for the files and packages you changed:
102
103```sh
104vp test run <files>
105vp lint <files>
106vp run --filter <package> typecheck
107```
108
109TypeScript workspaces typecheck with tsc-rs, which has Effect diagnostics built in.
110Keep `typescript`, `@effect/tsgo`, and the `effect-tsgo patch` prepare step: editors
111still use them for Effect quick fixes and refactors. Keep `@effect/tsgo` on the
112effect-tsgo version that tsc-rs ports (see the tsc-rs README), or the two disagree
113on rule names.
114Marketing keeps `astro check` for Astro files.
115
116Use `vp run lint:mobile` for native mobile changes. CI owns the full suite; see
117[ci.yml](../../.github/workflows/ci.yml) for its current jobs.
118The [manual Windows lane](../../.github/workflows/windows-tests.yml) is available for focused
119Windows investigation while that suite is not a required gate.
120
121### Unused code
122
123`vp run knip:check` checks unused files and dependencies across the repo, then
124unused runtime exports in `apps/server`, `apps/desktop`, `apps/web`, and every internal package under
125`packages/`. CI enforces both checks.
126Exported types and Effect schemas are allowed without consumers. The schema preprocessor
127recognizes schema types, including aliases and schema classes; functions that create or decode
128schemas remain checked. Canonical Effect service construction APIs stay exported with an explicit
129`@public` annotation, which Knip recognizes. Completely unused files remain checked too.
130Named exports in web UI component modules are kept as complete component sets. Knip ignores
131unused exports in `apps/web/src/components/ui/*.tsx`, while still reporting an entire unused file.
132Use `vp run knip --workspace apps/web` to audit one workspace, including exports,
133or `vp run knip:production --workspace apps/web` to find code kept alive only by tests.
134The full export audit still has findings and is not a repo-wide CI gate. Extend the
135export check's workspace selectors as more workspaces become clean. Review callers before
136deleting code; production mode can also report development scripts and test fixtures.
137Runtime-discovered entrypoints and dependency exceptions belong in [knip.jsonc](../../knip.jsonc).
138
139## Desktop artifacts
140
141Local artifact builds are unsigned by default and write to `release/`:
142
143```sh
144vp run dist:desktop:dmg
145vp run dist:desktop:linux
146vp run dist:desktop:win
147```
148
149DMGs default to the host architecture. Use `--arch` to choose another target and `--keep-stage`
150to retain packaging files for inspection. Run `vp run dist:desktop:artifact --help` for other
151options.
152
153### Linux AppImage prerequisites
154
155Build on Linux because the browser-secret helper links against the host's libsecret. Install
156Rust, C/C++ build tools, libsecret development headers, pkg-config, and ImageMagick.
157
158Ubuntu and Debian:
159
160```sh
161sudo apt-get update
162sudo apt-get install cargo rustc build-essential libsecret-1-dev pkg-config imagemagick
163```
164
165Fedora:
166
167```sh
168sudo dnf install rust cargo gcc gcc-c++ make libsecret-devel pkgconf-pkg-config ImageMagick
169```
170
171Arch Linux:
172
173```sh
174sudo pacman -S rust base-devel libsecret pkgconf imagemagick
175```
176
177The C toolchain, pkg-config, and libsecret headers are also needed for Linux desktop development.
178
179### macOS DMG prerequisites
180
181Install the Xcode Command Line Tools with `xcode-select --install` and install Rust.
182For a cross-architecture or universal build, add the requested Rust targets:
183
184```sh
185rustup target add aarch64-apple-darwin x86_64-apple-darwin
186```
187
188### Windows installer prerequisites
189
190Install Rust, Python 3, and Visual Studio Build Tools with **Desktop development with C++**.
191Include the Windows SDK and the MSVC build tools and Spectre-mitigated libraries for the target
192architecture. Add its Rust target:
193
194```powershell
195rustup target add x86_64-pc-windows-msvc
196# For an ARM64 installer:
197rustup target add aarch64-pc-windows-msvc
198```
199
200NSIS is downloaded by electron-builder. WSL support additionally needs the Linux CLI archive
201passed as `--wsl-runtime`; see the
202[release runbook](./release.md#windows-payload-topology-and-update-validation).
203
204### Signing and passkeys
205
206Add `--signed` after configuring the platform credentials in the
207[release runbook](./release.md). macOS passkeys need a signed, provisioned app; follow the
208[Connect setup](./connect-setup.md#desktop-passkeys) for local signing and renderer HMR.
209
210### Private fork releases for Apple Silicon
211
212Licensed Name Sans and Name Mono files live in the ignored
213`apps/web/public/fonts/private/` directory. Copy that directory into any other
214build workspace before building with these fonts. The web and desktop builds
215bundle the local files automatically; source checkouts without them use system
216fallback fonts. Keep the font binaries out of source history.
217
218Create `~/.config/t3-fork-release.json` with permissions `600`:
219
220```json
221{
222 "feedUrl": "https://files.example.com/read-only-share/",
223 "uploadUrl": "https://files.example.com/private-write-share/",
224 "uploadApiKey": "<Copyparty API password>",
225 "signingIdentity": "<40-character certificate fingerprint>",
226 "signingKeychain": "/Users/you/.config/t3-fork-signing.keychain-db"
227}
228```
229
230Only the public feed URL is bundled in the app. Keep the upload URL, API key, and
231signing credentials outside the repository. Uploads authenticate through Copyparty's
232`PW` header. The publishing account needs to create, overwrite, and delete files;
233the public share needs GET, HEAD, and Copyparty's `?ls` listing access to the same
234files. No application server or package registry is required for the feed.
235
236Import the signing certificate's `.p12` into a dedicated build keychain so signing
237works from Terminal without granting access to other login-keychain keys. Put its
238password in a private file such as `/tmp/certpass.txt`:
239
240```sh
241umask 077
242fork_keychain="$HOME/.config/t3-fork-signing.keychain-db"
243openssl rand -base64 32 > "$fork_keychain.password"
244security create-keychain -p "$(cat "$fork_keychain.password")" "$fork_keychain"
245security unlock-keychain -p "$(cat "$fork_keychain.password")" "$fork_keychain"
246security import "$HOME/Certificates.p12" -k "$fork_keychain" \
247 -P "$(cat /tmp/certpass.txt)" -T /usr/bin/codesign
248security set-key-partition-list -S apple-tool:,apple: -s \
249 -k "$(cat "$fork_keychain.password")" "$fork_keychain"
250security find-identity -v -p codesigning "$fork_keychain"
251```
252
253Copy the certificate fingerprint into `signingIdentity`. Releases unlock this
254keychain using its adjacent `.password` file. The fingerprint avoids selecting
255an older or revoked certificate with the same display name.
256
257Direct downloads that pass Gatekeeper need a **Developer ID Application**
258certificate from the same Apple Developer account used for iOS, plus Apple's
259notarization service. Apple Development and iOS distribution certificates do not
260replace Developer ID. Store notarization credentials in the build keychain with
261`xcrun notarytool store-credentials knife-notary --keychain "$fork_keychain"`,
262then add `"notarizationKeychainProfile": "knife-notary"` to the private release
263config and select the Developer ID certificate fingerprint. The release checks
264the certificate and credentials before building, signs with the hardened runtime,
265notarizes and staples the app, then verifies Gatekeeper acceptance. It stops if
266any of those checks fail. The notarization profile stays outside the repository.
267
268```sh
269vp run fork:build
270vp run fork:publish release/fork/0.0.<timestamp>
271# Build and publish together:
272vp run fork:release
273```
274
275Builds still verify a signed Apple Silicon app locally. Publishing separates UI,
276desktop application code, portable server code, and static assets into checksum-verified archives;
277unchanged archives are reused. The full app ZIP is uploaded only when Electron,
278the bootstrap, signing policy, or shell resources change. A UI edit therefore uploads
279the UI code archive and small release manifests. Static models, fonts, and images
280are cached separately across UI edits. Versions increase independently of
281upstream versions and rebases. Publishing makes every referenced archive readable,
282then creates the immutable version manifest. Clients select the newest completed
283version from the directory listing. Run one publisher at a time: installer URLs are
284mutable, so simultaneous installer replacement is unsupported.
285The root `preadme.md` links to the available platform installers and is refreshed
286after a completed publication. Version-free download entrypoints use
287`The-Knife-darwin-arm64.zip`, `The-Knife-linux-x64.AppImage`, and
288`The-Knife-win32-x64.exe`. Publication uploads replacements under temporary names,
289verifies their checksums, then promotes them with WebDAV MOVE. Native versions
290and checksums remain in the completion JSON and per-version updater YAML.
291Before replacement, conflicting completed releases and unreferenced updater YAML
292are retired. The two newest nonconflicting completed releases and their referenced
293components are retained; unrelated files and incomplete uploads are left alone.
294If publication fails after retirement, retry the same local release directory.
295Its atomically written `publication.json` preserves the merged release and cleanup
296ownership until the exact completion and all dependencies are verified.
297
298Build Linux installers on Linux for the native capture and credential helpers.
299Windows builds can run natively, or use matching prebuilt helpers with
300`T3CODE_DESKTOP_WINDOWS_RUNTIME` pointing to a Wine launcher for the isolated
301backend probe. Native Windows builds also exercise the packaged file index.
302Export the packaged resources alongside the installer and updater manifest:
303
304```sh
305# On Linux (use --platform win --target nsis on Windows):
306T3CODE_DESKTOP_FORK_RESOURCES=true T3CODE_DESKTOP_UPDATE_URL=https://files.example.com/read-only-share/ \
307 node scripts/build-desktop-artifact.ts --platform linux --target AppImage \
308 --arch x64 --build-version 0.0.<timestamp> --output-dir /path/to/native-build
309```
310
311Copy that output directory to the publishing host, then run:
312
313```sh
314node scripts/release-fork.ts ingest linux-x64 /path/to/native-build
315# Windows:
316node scripts/release-fork.ts ingest win32-x64 /path/to/native-build
317```
318
319Ingestion verifies the installer and resource versions, separates reusable
320components, and publishes a new completed manifest. Each platform keeps its own
321backend, UI, and asset versions until its next native build is published.
322
323Install the generated `.app` once on each Mac to bootstrap this updater. Subsequent
324code updates download without replacing Electron. Application code and shell changes
325require a confirmed restart. UI updates can apply after one minute unfocused or ten
326minutes idle once restorable state is saved; live browser guests and other state
327that cannot be restored defer automatic refresh.
328The Knife retains the shipped bundle ID, data directory, credential storage, and
329Connect configuration. Its display name and new artifacts use The Knife; the
330existing updater accepts the new artifact filenames. The initial
331build must include any public Connect configuration used by that installation;
332the build loads the repository's local environment files.
333
334`security find-identity -v -p codesigning` lists usable signing identities. A
335certificate needs its private key in the keychain; a `.p8` App Store Connect key
336alone cannot sign an app. Keep the same signing identity across releases. A local
337Apple Development identity still signs private builds, with a build warning that
338Gatekeeper may block downloaded copies. Native passkeys additionally need the
339provisioned signing procedure linked above.
340
341### Private server container
342
343The [Compose configuration](../../compose.yaml) runs a persistent Knife server,
344including the private shared-account authority. The public
345`knife-server-linux-amd64.tar.gz` deployment bundle contains the image, this Compose
346configuration, and a public-feed environment example. Verify it against its
347adjacent `.sha256` file, extract it, and load `image.tar` with
348`docker image load -i image.tar`. Copy `.env.example` to `.env` and prepare the
349archive and workspace directories with write access for UID 1000. Start it with
350`docker compose up -d --no-build`; the deployment bundle needs no source checkout
351or registry account.
352
353To build the base image from source, run `docker build -t knife:local .` on the
354target Linux architecture. `docker image save knife:local` exports a loadable image.
355A registry can supply `KNIFE_IMAGE` instead.
356
357Set `KNIFE_UPDATE_URL` to the public release directory,
358`KNIFE_ARCHIVES_DIRECTORY` and `KNIFE_WORKSPACES_DIRECTORY` to host directories,
359then run `docker compose up -d`. The default port binds to localhost for a reverse
360proxy; set `KNIFE_BIND_ADDRESS` for a different binding. `/data` and the provider home
361use persistent volumes. Keep both private and backed up; account credentials never
362belong on the public build share.
363
364At startup the container downloads only missing server/UI/assets archives from the same
365feed, checks their hashes and native dependency contract, and runs the selected
366release. `docker compose restart knife` applies a published code update after
367confirming the restart with Clover. While running it prefetches new archives every
368four minutes without restarting or replacing the running code. A feed outage uses the cached release or bundled
369baseline; a checksum or dependency mismatch stops startup. Rebuild the base image
370when the server's locked native dependency closure or container bootstrap changes.
371
372Use hosted sign-in below for desktop enrollment and archive uploads. Optional
373[shared provider accounts](../user/shared-server.md#share-agent-accounts) use separate
374grants; signing into the server does not export local provider credentials.
375
376### Hosted sign-in
377
378Register a confidential OpenID Connect application with your identity provider:
379
380| Setting | Value |
381| --------------------- | ------------------------------------------------------------------- |
382| Grant types | `authorization_code`, `refresh_token` |
383| Response type | `code` |
384| Callback | `https://knife.example.com/api/auth/oidc/callback`, matched exactly |
385| PKCE | Required, `S256` |
386| Client authentication | `client_secret_basic` |
387| ID token algorithm | `RS256` |
388| Scopes | `openid offline_access`; allow refresh tokens |
389
390The issuer callback is HTTPS. `t3code://app/self-hosted/callback` is a separate,
391proof-bound return to the desktop; do not register it with the issuer. There is no
392public native client or dynamic client registration.
393
394Set `T3CODE_SSO_CONFIG` to a private JSON file readable by the server process:
395
396```json
397{
398 "publicUrl": "https://knife.example.com",
399 "issuer": "https://identity.example.com",
400 "clientId": "knife",
401 "clientSecret": "PRIVATE_REGISTERED_CLIENT_SECRET",
402 "ownerSub": "IMMUTABLE_OWNER_SUBJECT",
403 "maxInteractiveAgeSeconds": 2592000,
404 "issuerRevalidationSeconds": 3600
405}
406```
407
408Use the exact issuer string from discovery and its immutable subject for the
409intended owner. `publicUrl` is the HTTPS origin without a trailing slash. Email,
410display name, and the first person to visit never establish ownership. Missing
411owner configuration prevents SSO from starting. Without `T3CODE_SSO_CONFIG`,
412existing pairing remains available.
413
414Keep this file outside the checkout, restrict it to its owner (`chmod 600`), and
415mount it read-only into containers. Pass its container path through
416`T3CODE_SSO_CONFIG`. Never include it in an image, public build share, or browser
417configuration. The same deployment needs persistent private server state and an
418HTTPS proxy that forwards WebSocket upgrades.
419
420For Compose, set `KNIFE_SSO_CONFIG` to that host file's absolute path and
421`KNIFE_ARCHIVE_STORE_ID` to the identity stored in the NAS archive directory's
422pre-existing `.knife-archive-store-id` file. The server never creates this marker;
423missing or mismatched storage identity prevents catalog publication and local
424payload eviction. Use the optional overlay:
425
426```sh
427docker compose -f compose.yaml -f compose.sso.yaml up -d --no-build
428```
429
430The overlay mounts the private configuration read-only and refuses to create a
431missing host file. Existing deployments can keep using the base Compose file.
432
433Access credentials last fifteen minutes. Native devices renew with rotating
434credentials kept in the operating system’s protected storage. By default, devices
435must complete browser sign-in again after thirty days. The server revalidates the
436issuer grant during renewal at least once per hour; issuer-side revocation is
437noticed on that check, and previously issued access can last another fifteen
438minutes. Local device revocation closes active connections and disables renewal.
439
440If the issuer is unavailable or a device is lost, run this on the server using its
441existing local state directory:
442
443```sh
444t3 auth devices revoke-all
445```
446
447This local recovery command revokes SSO devices and sessions without contacting
448the issuer. Existing pairing credentials remain valid. It does not change the
449configured owner or revoke identity-provider accounts.