Superseded: This project has been superseded by
schwab-auth-daemon.New development should happen there. This repository is kept for historical/reference purposes and existing users, but it is no longer the recommended starting point for new setups.
Centralised OAuth token daemon for @misterpea/schwab-node.
Schwab allows one active OAuth session per account. If multiple services each try to manage their own auth, they will invalidate each other's tokens.
Run one daemon that owns all token work. Every other service reads the current token from the system keychain — no OAuth flow, no conflicts.
┌─────────────────────────────────┐
│ schwab-auth-daemon │ ← this package
│ • holds SchwabAuth (managed) │
│ • refreshes token every 20 min │
│ • writes token to keychain │
└────────────────┬────────────────┘
│ keychain
┌───────────┴───────────┐
▼ ▼
Service A Service B
createDelegatedAuth() createDelegatedAuth()
reads keychain reads keychain
npm install @misterpea/schwab-node-persistent-authCopy .env.example to .env and fill in your Schwab app credentials:
SCHWAB_CLIENT_ID=your_client_id
SCHWAB_CLIENT_SECRET=your_client_secret
SCHWAB_REDIRECT_URI=https://127.0.0.1:8443
# Optional
SCHWAB_REFRESH_INTERVAL_MS=1200000
SCHWAB_KEYCHAIN_SERVICE=schwab-nodeNote: Node's
--env-fileflag includes everything after=as the value, including inline# comments. Put comments on their own lines only.
# Production
node --env-file .env node_modules/.bin/schwab-auth-daemon
# Dev (TypeScript, no build step)
npm run devOn first run the daemon will open a browser for the initial Schwab OAuth login. After that it runs silently, refreshing the access token on the configured interval and writing it to the system keychain.
Keychain support: macOS Keychain, Windows Credential Manager, Linux libsecret (via
keytar).
Instead of managing auth yourself, point createDelegatedAuth at the keychain:
import { createDelegatedAuth } from '@misterpea/schwab-node';
import { KeychainTokenStore } from '@misterpea/schwab-node-persistent-auth';
const auth = createDelegatedAuth(new KeychainTokenStore('schwab-node'));That's it. The service reads the token the daemon keeps fresh — no credentials, no refresh logic, no browser prompts.
This package ships a control CLI for managing the daemon when it runs as a launchd service (macOS) or similar system supervisor.
Make the command available globally by linking the package:
cd /path/to/your/schwab-auth-daemon-project
npm link @misterpea/schwab-node-persistent-authOr if you cloned this repo directly:
npm linkThen use it from anywhere:
schwab-daemon-ctl <command>| Command | Description |
|---|---|
start |
Start the daemon via launchctl |
stop |
Send SIGTERM to the daemon (launchd will restart it if KeepAlive=true) |
status |
Show whether the daemon is running and its PID |
auth |
Clear the stored token and restart the daemon to trigger a fresh OAuth browser flow |
Use auth whenever you need to re-authenticate — for example after an extended absence when the refresh token (7-day lifetime) has expired.
# Check if daemon is healthy
schwab-daemon-ctl status
# Force re-authentication (opens browser)
schwab-daemon-ctl authThe auth command respects the same environment variables as the daemon:
SCHWAB_DAEMON_LABEL=com.my-daemon schwab-daemon-ctl auth| Event | Action |
|---|---|
| Startup | Calls getAuth() immediately |
| Every interval | Calls getAuth() — refreshes if access token is expired |
| Refresh token nearing expiry (within 48 h of 7-day limit) | Clears token, opens browser for full re-auth |
| SIGTERM / SIGINT | Graceful shutdown |
| Variable | Default | Description |
|---|---|---|
SCHWAB_CLIENT_ID |
— | Schwab app client ID (required) |
SCHWAB_CLIENT_SECRET |
— | Schwab app client secret (required) |
SCHWAB_REDIRECT_URI |
— | OAuth redirect URI (required) |
SCHWAB_REFRESH_INTERVAL_MS |
1200000 |
Token refresh interval in ms. Keep under 30 min (access token lifetime). |
SCHWAB_KEYCHAIN_SERVICE |
schwab-node |
Keychain namespace. Must match the name passed to new KeychainTokenStore() in every consumer app. |
SCHWAB_DAEMON_LABEL |
com.schwab-auth-daemon |
launchd service label — used by schwab-daemon-ctl only. |