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