# Tuna > Cloudflare Tunnels for humans ## Access Control Tuna integrates with Cloudflare Access to restrict who can access your tunnels. Control access through your `package.json` - no dashboard required. ### Overview By default, tunnels are public. Anyone with the URL can access your dev server. Add the `access` field to require authentication: ```json { "tuna": { "forward": "staging.example.com", "port": 3000, "access": ["@company.com"] } } ``` Now only people with `@company.com` emails can access the tunnel. ### How It Works 1. User visits your tunnel URL 2. Cloudflare Access intercepts the request 3. User enters their email 4. They receive a one-time PIN via email 5. After verification, they can access your dev server 6. Session lasts 24 hours by default ### Configuration #### Allow Email Domain Use `@domain` to allow all emails from a domain: ```json { "tuna": { "access": ["@company.com"] } } ``` This allows: * `alice@company.com` ✅ * `bob@company.com` ✅ * `eve@gmail.com` ❌ #### Allow Specific Emails List individual email addresses: ```json { "tuna": { "access": ["alice@gmail.com", "bob@outlook.com"] } } ``` #### Mixed Rules Combine domains and specific emails: ```json { "tuna": { "access": [ "@company.com", "contractor@gmail.com", "client@clientcorp.com" ] } } ``` ### Examples #### Internal Development Only your team can access: ```json { "tuna": { "forward": "$USER-dev.example.com", "port": 3000, "access": ["@mycompany.com"] } } ``` #### Client Demo Your team plus specific client contacts: ```json { "tuna": { "forward": "demo.example.com", "port": 3000, "access": [ "@mycompany.com", "product@clientcorp.com", "engineering@clientcorp.com" ] } } ``` #### Contractor Access Specific external people: ```json { "tuna": { "forward": "staging.example.com", "port": 3000, "access": [ "@mycompany.com", "contractor1@freelance.com", "contractor2@agency.com" ] } } ``` #### Public Access Omit the `access` field for public tunnels: ```json { "tuna": { "forward": "public-demo.example.com", "port": 3000 } } ``` ### Updating Access Simply change the `access` array and restart tuna: ```json { "tuna": { "access": [ "@company.com", "newperson@external.com" // Added ] } } ``` ```bash # Restart to apply changes tuna npm run dev ``` Tuna automatically syncs the access policy with Cloudflare. ### Removing Access Control Remove the `access` field to make the tunnel public: ```json { "tuna": { "forward": "my-app.example.com", "port": 3000 // No "access" field = public } } ``` The Access application will be automatically deleted. ### User Experience When someone visits a protected tunnel: 1. **Access page** - Cloudflare shows a branded login page 2. **Email input** - User enters their email 3. **Verification** - They receive a 6-digit code via email 4. **Access granted** - After entering the code, they're in The session is remembered for 24 hours (configurable in Cloudflare dashboard). ### Troubleshooting #### "Access Denied" for Valid User Check that: * Email is spelled correctly in config * For domain rules, ensure the `@` prefix is included * User is checking the correct email inbox #### User Not Receiving Code * Check spam/junk folder * Cloudflare emails come from `noreply@notify.cloudflare.com` * Some corporate email filters block these #### Access Not Updating If changes don't take effect: ```bash # Stop the current tunnel tuna --stop # Start again tuna npm run dev ``` ### Limitations * **Authentication method**: Currently only email OTP (no SSO/SAML) * **Session duration**: 24 hours (configurable in Cloudflare dashboard only) * **Rate limits**: Cloudflare Access free tier has usage limits ### Advanced: Cloudflare Dashboard For more advanced policies (SSO, device posture, etc.), use the Cloudflare Zero Trust dashboard directly. Tuna-created applications appear under: **Zero Trust → Access → Applications** You can modify them there, but changes may be overwritten next time tuna runs. ## Configuration Tuna is configured through the `tuna` field in your `package.json`. ### Basic Configuration ```json { "name": "my-app", "tuna": { "forward": "my-app.example.com", "port": 3000 } } ``` ### Configuration Options #### `forward` (required) The domain where your tunnel will be accessible. ```json { "tuna": { "forward": "my-app.example.com", "port": 3000 } } ``` **Rules:** * Must be a valid domain name * Must be a subdomain of a domain in your Cloudflare account * Supports environment variable interpolation (see below) #### `port` (required) The local port your dev server runs on. ```json { "tuna": { "forward": "my-app.example.com", "port": 3000 } } ``` **Rules:** * Must be a number between 1 and 65535 * Should match the port your dev server uses #### `access` (optional) Array of email addresses or domains that can access the tunnel. ```json { "tuna": { "forward": "my-app.example.com", "port": 3000, "access": ["@company.com", "contractor@gmail.com"] } } ``` **Patterns:** * `@domain.com` - Allow all emails from domain * `user@domain.com` - Allow specific email See [Access Control](/guide/access-control) for details. ### Environment Variables The `forward` field supports environment variable interpolation: | Variable | Description | Example | | ------------ | ----------------------------------------- | -------------- | | `$USER` | Current username | `alice` | | `$TUNA_USER` | Custom identifier (falls back to `$USER`) | `alice` | | `$HOME` | Home directory | `/Users/alice` | #### Team Configuration Use `$USER` to give each developer their own subdomain: ```json { "tuna": { "forward": "$USER-api.example.com", "port": 3000 } } ``` Results: * Alice → `alice-api.example.com` * Bob → `bob-api.example.com` #### Custom Identifier Override the username with `TUNA_USER`: ```bash TUNA_USER=staging tuna npm run dev # → staging-api.example.com ``` ### Examples #### Basic Web App ```json { "tuna": { "forward": "myapp.example.com", "port": 3000 } } ``` #### API Server for Team ```json { "tuna": { "forward": "$USER-api.example.com", "port": 8080 } } ``` #### Staging Environment with Access Control ```json { "tuna": { "forward": "staging.example.com", "port": 3000, "access": ["@mycompany.com"] } } ``` #### Client Demo ```json { "tuna": { "forward": "demo.example.com", "port": 3000, "access": ["client@clientcompany.com", "@mycompany.com"] } } ``` ### Multiple Environments For different environments, you can use different branches or environment variables: ```bash # Development (each dev gets their own) # package.json: "forward": "$USER-dev.example.com" tuna npm run dev # Staging TUNA_USER=staging tuna npm run dev # → staging-dev.example.com ``` ### Validation Tuna validates your configuration on startup: * `forward` must be a valid domain after variable interpolation * `port` must be a number between 1-65535 * `access` entries must be valid email addresses or `@domain` patterns Invalid configuration will show a helpful error message. ## Getting Started This guide will help you set up Tuna and create your first tunnel in under 5 minutes. ### Prerequisites * Node.js 18 or higher * A Cloudflare account with a domain * macOS (Linux/Windows support coming soon) ### Installation :::code-group ```bash [npm] npm install -g tuna ``` ```bash [pnpm] pnpm add -g tuna ``` ::: ### Setup #### 1. Create a Cloudflare API Token Visit [Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens) and create a token with these permissions: | Scope | Permission | | ----------------------------------- | ---------- | | Account → Cloudflare Tunnel | Edit | | Account → Access: Apps and Policies | Edit | | Zone → DNS | Edit | | Account → Account Settings | Read | #### 2. Login to Tuna ```bash tuna --login ``` Enter your API token and domain when prompted. Credentials are stored securely in your macOS Keychain. #### 3. Configure Your Project Run the interactive setup: ```bash tuna --init ``` Or manually add to your `package.json`: ```json { "tuna": { "forward": "my-app.example.com", "port": 3000 } } ``` #### 4. Run Your Dev Server ```bash tuna npm run dev ``` Your local server is now available at `https://my-app.example.com`! ### Next Steps * Team Collaboration - Set up unique URLs per developer * Access Control - Restrict who can access your tunnel * Configuration Reference - All configuration options ## Installation ### Requirements * **Node.js 18+** - Tuna uses modern Node.js features * **macOS** - Currently the only supported platform (Linux/Windows coming soon) * **Cloudflare account** - With at least one domain added ### Install from npm :::code-group ```bash [npm] npm install -g tuna ``` ```bash [pnpm] pnpm add -g tuna ``` ```bash [yarn] yarn global add tuna ``` ::: ### Verify Installation ```bash tuna --version ``` ### Using npx You can also use tuna without installing globally: ```bash npx tuna npm run dev ``` ### First-Time Setup After installation, you need to configure your Cloudflare credentials: #### 1. Create a Cloudflare API Token 1. Go to [Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens) 2. Click "Create Token" 3. Use "Create Custom Token" 4. Add these permissions: | Scope | Resource | Permission | | ------- | ------------------------- | ---------- | | Account | Cloudflare Tunnel | Edit | | Account | Access: Apps and Policies | Edit | | Zone | DNS | Edit | | Account | Account Settings | Read | 5. Under "Zone Resources", select your domain or "All zones" 6. Click "Continue to summary" → "Create Token" 7. **Copy the token** (you won't see it again!) #### 2. Run Login ```bash tuna --login ``` You'll be prompted for: * Your API token * Your root domain (e.g., `example.com`) Credentials are stored securely in your macOS Keychain. #### 3. Initialize Your Project ```bash cd your-project tuna --init ``` Or manually add to `package.json`: ```json { "tuna": { "forward": "my-app.example.com", "port": 3000 } } ``` #### 4. Start Using Tuna ```bash tuna npm run dev ``` ### Cloudflared Binary Tuna needs the `cloudflared` binary to create tunnels. It will: 1. Check if `cloudflared` is already installed (via Homebrew, etc.) 2. If not found, automatically download it to `~/.tuna/bin/` To install cloudflared manually: ```bash # macOS with Homebrew brew install cloudflared # Or download directly curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-amd64 -o /usr/local/bin/cloudflared chmod +x /usr/local/bin/cloudflared ``` ### Updating ```bash npm update -g tuna ``` ### Uninstalling ```bash # Remove the package npm uninstall -g tuna # Optional: Remove credentials from Keychain # Open Keychain Access, search for "tuna-credentials" # Optional: Remove tuna data rm -rf ~/.tuna ``` ## Team Collaboration One of tuna's best features is built-in support for team development. Each developer automatically gets their own subdomain, preventing conflicts. ### The Problem When multiple developers work on the same project with tunnels: * **Subdomain conflicts** - Two people can't use `api.example.com` at the same time * **Manual coordination** - "Hey, are you using the tunnel right now?" * **DNS collisions** - Overwriting each other's DNS records ### The Solution: $USER Variable Use the `$USER` environment variable in your forward domain: ```json { "tuna": { "forward": "$USER-api.example.com", "port": 3000 } } ``` Now each developer automatically gets their own subdomain: | Developer | Username | Tunnel URL | | --------- | --------- | --------------------------------- | | Alice | `alice` | `https://alice-api.example.com` | | Bob | `bob` | `https://bob-api.example.com` | | Charlie | `charlie` | `https://charlie-api.example.com` | ### Setup #### 1. Configure package.json ```json { "tuna": { "forward": "$USER-api.example.com", "port": 3000 } } ``` #### 2. Each Developer Logs In Each team member runs this once: ```bash tuna --login ``` They'll need: * The same Cloudflare API token (shared) or their own token with appropriate permissions * The root domain (`example.com`) #### 3. Run Normally ```bash tuna npm run dev ``` Each developer sees their own URL: ``` ✓ Tunnel active: https://alice-api.example.com → localhost:3000 ``` ### Patterns #### API Server ```json { "tuna": { "forward": "$USER-api.example.com", "port": 8080 } } ``` #### Frontend + Backend For a monorepo with multiple services: ```json // packages/frontend/package.json { "tuna": { "forward": "$USER-app.example.com", "port": 3000 } } // packages/backend/package.json { "tuna": { "forward": "$USER-api.example.com", "port": 8080 } } ``` #### Feature Branches Use `$TUNA_USER` for feature-specific tunnels: ```bash # Working on feature-auth TUNA_USER=auth tuna npm run dev # → auth-api.example.com # Working on feature-payments TUNA_USER=payments tuna npm run dev # → payments-api.example.com ``` ### Sharing Credentials #### Option 1: Shared API Token Create one API token and share it with the team: **Pros:** * Simple setup * One token to manage **Cons:** * Can't revoke access for one person * Harder to audit who did what #### Option 2: Individual Tokens Each developer creates their own token: **Pros:** * Can revoke individual access * Better audit trail * Follows principle of least privilege **Cons:** * More setup per person #### Token Permissions Needed Each token needs: | Scope | Resource | Permission | | ------- | ------------------------- | ---------- | | Account | Cloudflare Tunnel | Edit | | Account | Access: Apps and Policies | Edit | | Zone | DNS | Edit | | Account | Account Settings | Read | ### Listing Team Tunnels See all tunnels for your domain: ```bash tuna --list ``` Output: ``` TUNNELS for example.com (3) NAME STATUS DOMAIN CREATED tuna-alice-api-example-com ● active alice-api.example.com 2024-01-15 tuna-bob-api-example-com ● active bob-api.example.com 2024-01-15 tuna-charlie-api-example-com ○ down charlie-api.example.com 2024-01-14 ``` ### Cleanup Developers can clean up their own tunnels: ```bash # Delete current project's tunnel tuna --delete # Delete specific tunnel tuna --delete tuna-alice-api-example-com ``` ### Best Practices 1. **Use consistent naming** - Agree on a pattern like `$USER-{service}.example.com` 2. **Document the setup** - Add tuna setup to your project's README 3. **Share the domain** - Everyone should use the same root domain 4. **Clean up old tunnels** - Delete tunnels when leaving a project ## Why Tuna? There are many ways to expose your local development server to the internet. Here's why tuna might be the right choice for you. ### The Problem When developing modern web applications, you often need to expose your local server to the internet for: * **Webhook testing** - Stripe, GitHub, Twilio all need to send data to your app * **OAuth callbacks** - Social login providers need a real URL * **Mobile testing** - Test on real devices without complex network config * **Client demos** - Share work-in-progress securely * **Team collaboration** - Multiple devs working on the same project The traditional solutions each have drawbacks: #### ngrok * Random URLs on free tier * Custom domains require paid plan ($8+/month) * Tunnels don't persist across restarts * No native access control #### localtunnel * Random subdomains only * Unreliable uptime * No authentication * Often blocked by corporate firewalls #### Raw cloudflared * Complex setup with multiple commands * Manual DNS configuration * Dashboard required for access control * No command wrapping ### The Solution Tuna wraps cloudflared with a developer-friendly interface: ```bash # Instead of this... cloudflared tunnel create my-tunnel cloudflared tunnel route dns my-tunnel my-app.example.com cloudflared tunnel run my-tunnel & npm run dev # Just do this tuna npm run dev ``` ### Feature Comparison | Feature | tuna | ngrok | localtunnel | cloudflared | | --------------------------- | :--: | :---: | :---------: | :---------: | | Free custom domains | ✅ | ❌ | ❌ | ✅ | | Persistent tunnels | ✅ | ❌ | ❌ | ✅ | | Zero config | ✅ | ❌ | ✅ | ❌ | | Wrapper mode | ✅ | ❌ | ❌ | ❌ | | Team collaboration | ✅ | ❌ | ❌ | ❌ | | Config-based access control | ✅ | ❌ | ❌ | ❌ | | Secure credential storage | ✅ | ✅ | N/A | ❌ | | No account required | ❌ | ✅ | ✅ | ❌ | ### Key Advantages #### 1. Free Custom Domains Use your own domain at no extra cost. If you have `example.com` on Cloudflare, you can create unlimited subdomains: ```json { "tuna": { "forward": "dev.example.com", "port": 3000 } } ``` #### 2. Persistent Tunnels Tuna installs cloudflared as a system service. Your tunnel survives: * Terminal restarts * SSH disconnections * System reboots (with auto-start) #### 3. Team-Friendly The `$USER` variable prevents subdomain conflicts: ```json { "tuna": { "forward": "$USER-api.example.com", "port": 3000 } } ``` * Alice gets `alice-api.example.com` * Bob gets `bob-api.example.com` * No coordination needed #### 4. Config-Driven Access Control Restrict access without touching a dashboard: ```json { "tuna": { "forward": "staging.example.com", "port": 3000, "access": ["@mycompany.com", "client@gmail.com"] } } ``` #### 5. Transparent Wrapper Tuna doesn't interfere with your dev tools: * Full color output preserved * TTY/interactive mode works * Exit codes passed through * Ctrl+C works as expected ### When NOT to Use Tuna Tuna might not be right for you if: * **You don't have a Cloudflare account** - Tuna requires a domain on Cloudflare * **You need Windows/Linux support** - Currently macOS only * **You need one-off tunnels** - ngrok's random URLs are simpler for quick shares * **You're in a restricted network** - Some corporate networks block Cloudflare tunnels ### Cost Tuna itself is free and open source. The underlying Cloudflare services are also free: * Cloudflare Tunnels: Free * Cloudflare DNS: Free * Cloudflare Access (up to 50 users): Free You just need a domain on Cloudflare (can be a cheap $10/year domain). ## tuna --delete Delete a tunnel and clean up all associated resources. ### Usage ```bash # Delete tunnel for current project tuna --delete # Delete specific tunnel by name tuna --delete ``` ### Examples ```bash # Delete based on package.json config tuna --delete # Delete specific tunnel tuna --delete tuna-alice-api-example-com ``` ### What It Does 1. Identifies the tunnel (from config or argument) 2. Authenticates with Cloudflare 3. Asks for confirmation 4. Stops the service if running 5. Deletes the tunnel from Cloudflare 6. Removes DNS records 7. Removes local credential files 8. Removes config files 9. Uninstalls service if no other tunnels ### Output ```bash $ tuna --delete 🔐 Touch ID required Found tunnel: tuna-alice-api-example-com Domain: alice-api.example.com Created: 2024-01-23 ⚠ This will: - Delete the tunnel from Cloudflare - Remove DNS records - Delete local configuration ? Continue? Yes ✓ Tunnel deleted ✓ DNS records removed ✓ Local files removed ✓ Service uninstalled ``` ### Skip Confirmation Use `--yes` or `-y` to skip confirmation: ```bash tuna --delete --yes tuna --delete tuna-old-tunnel --yes ``` ### Finding Tunnel Names List tunnels to find the name: ```bash $ tuna --list NAME STATUS DOMAIN tuna-alice-api-example-com ● active alice-api.example.com tuna-old-project-example-com ○ down old-project.example.com $ tuna --delete tuna-old-project-example-com ``` ### What Gets Deleted | Resource | Location | Deleted? | | ------------------ | ---------------------- | -------- | | Tunnel | Cloudflare | ✅ | | DNS CNAME record | Cloudflare | ✅ | | Access Application | Cloudflare | ✅ | | Tunnel credentials | `~/.tuna/tunnels/` | ✅ | | Ingress config | `~/.tuna/config-*.yml` | ✅ | | API credentials | Keychain | ❌ (kept) | ### What's NOT Deleted * Your Cloudflare account credentials (in Keychain) * Other tunnels * The `tuna` config in package.json ### Use Cases #### Clean Up Old Projects Remove tunnels for projects you're no longer working on: ```bash tuna --list tuna --delete tuna-old-project-example-com ``` #### Start Fresh Delete and recreate a tunnel: ```bash tuna --delete tuna npm run dev # Creates new tunnel ``` #### Team Member Leaving Clean up a team member's tunnel: ```bash tuna --delete tuna-bob-api-example-com ``` ### Error Handling #### Tunnel Not Found ```bash $ tuna --delete tuna-nonexistent Error: Tunnel 'tuna-nonexistent' not found ``` #### No package.json When running without a tunnel name: ```bash $ tuna --delete Error: No package.json found. Specify tunnel name: tuna --delete ``` ### See Also * [tuna --list](/commands/list) - Find tunnel names * [tuna --stop](/commands/stop) - Stop without deleting ## tuna --init Interactive setup wizard for configuring tuna in your project. ### Usage ```bash tuna --init ``` ### What It Does 1. Checks for existing `package.json` 2. Detects if tuna is already configured 3. Prompts for configuration options 4. Writes config to `package.json` ### Interactive Flow ```bash $ tuna --init 🐟 Tuna Project Setup ? Use $USER variable for unique subdomains per developer? Yes ? Subdomain pattern (will become $USER-.example.com): api ? Local port to forward: 3000 ? Enable Zero Trust Access control? Yes ? Access rules (comma-separated emails or @domains): @mycompany.com ✓ Configuration saved to package.json Added to package.json: { "tuna": { "forward": "$USER-api.example.com", "port": 3000, "access": ["@mycompany.com"] } } Next steps: Run: tuna npm run dev Your tunnel URL will be: https://alice-api.example.com ``` ### Options #### Subdomain Pattern Choose a pattern for your subdomain: * With `$USER`: `$USER-api.example.com` → `alice-api.example.com` * Without: `api.example.com` (fixed subdomain) #### Port The local port your dev server runs on. Common defaults: | Framework | Default Port | | ---------------- | ------------ | | Vite | 5173 | | Next.js | 3000 | | Create React App | 3000 | | Express | 3000 | | FastAPI | 8000 | #### Access Control Optionally restrict who can access your tunnel: * `@company.com` - Anyone with that email domain * `user@example.com` - Specific email addresses ### Overwriting Existing Config If tuna is already configured, you'll be asked to confirm: ```bash $ tuna --init ? Tuna config already exists. Overwrite? No Keeping existing config. ``` ### Manual Alternative Instead of `--init`, you can manually add to `package.json`: ```json { "name": "my-app", "tuna": { "forward": "$USER-api.example.com", "port": 3000, "access": ["@mycompany.com"] } } ``` ### Prerequisites * Must have a `package.json` in current directory * Should have run `tuna --login` first (for domain suggestion) ### See Also * [Configuration](/guide/configuration) - All config options * [tuna --login](/commands/login) - Set up credentials first * [Team Collaboration](/guide/team-collaboration) - Using $USER for teams ## tuna --list List all Cloudflare tunnels for your account. ### Usage ```bash tuna --list ``` ### Output ```bash $ tuna --list 🔐 Touch ID required TUNNELS for example.com (4) NAME STATUS DOMAIN CREATED tuna-alice-api-example-com ● active alice-api.example.com 2024-01-23 10:49 tuna-bob-api-example-com ● active bob-api.example.com 2024-01-23 11:15 tuna-staging-example-com ● active staging.example.com 2024-01-22 15:30 tuna-old-project-example-com ○ down old-project.example.com 2024-01-20 08:15 ● = active ○ = down ``` ### Status Indicators | Symbol | Status | Meaning | | ------ | ------ | -------------------------------- | | ● | active | Tunnel has active connections | | ○ | down | Tunnel exists but no connections | ### What It Shows * **NAME** - Tunnel identifier (auto-generated by tuna) * **STATUS** - Whether the tunnel is currently connected * **DOMAIN** - DNS record pointing to the tunnel * **CREATED** - When the tunnel was created ### Multiple Domains If you have credentials for multiple domains, you'll be prompted: ```bash $ tuna --list Select domain: 1. example.com (4 tunnels) 2. another-domain.com (2 tunnels) > All domains ``` ### Filtering by Project The list shows all tuna-created tunnels. Tuna tunnels are named with the pattern: ``` tuna-{sanitized-forward-domain} ``` ### Use Cases #### See Team Activity Check which team members have active tunnels: ```bash $ tuna --list ``` Tunnels with `$USER` in the config show each developer's username. #### Find Old Tunnels Identify tunnels that are no longer needed: ```bash $ tuna --list # Look for "down" status or old creation dates ``` #### Verify Tunnel Status Check if your tunnel is running: ```bash $ tuna --list # Find your tunnel and check status ``` ### Related Commands * [tuna --delete](/commands/delete) - Remove a tunnel * [tuna --stop](/commands/stop) - Stop the tunnel service ### See Also * [Team Collaboration](/guide/team-collaboration) - Managing team tunnels ## tuna --login Set up Cloudflare credentials for tunnel management. ### Usage ```bash tuna --login ``` ### What It Does 1. Prompts for your Cloudflare API token 2. Validates the token against Cloudflare API 3. Fetches your account ID 4. Prompts for your root domain 5. Verifies domain access 6. Stores credentials in macOS Keychain ### Interactive Flow ```bash $ tuna --login 🔐 Tuna Login Create a token at: https://dash.cloudflare.com/profile/api-tokens Required permissions: • Account → Cloudflare Tunnel → Edit • Account → Access: Apps and Policies → Edit • Zone → DNS → Edit • Account → Account Settings → Read ? Enter your Cloudflare API token: ************************************ ✓ Token validated ✓ Account ID: 699d98642c564d2e855e9661899b7252 ? Enter your root domain (e.g., example.com): example.com ✓ Domain verified ✓ Credentials saved to macOS Keychain Next steps: 1. Add to your package.json: { "tuna": { "forward": "my-app.example.com", "port": 3000 } } 2. Run: tuna npm run dev ``` ### Creating an API Token 1. Go to [Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens) 2. Click **Create Token** 3. Click **Create Custom Token** 4. Configure permissions: | Section | Resource | Permission | | ----------- | ----------------------------------- | ---------- | | Permissions | Account - Cloudflare Tunnel | Edit | | Permissions | Account - Access: Apps and Policies | Edit | | Permissions | Zone - DNS | Edit | | Permissions | Account - Account Settings | Read | 5. Under **Zone Resources**, select your domain or "All zones" 6. Click **Continue to summary** → **Create Token** 7. Copy the token immediately (it won't be shown again) ### Credential Storage Credentials are stored in your macOS Keychain: * **Service**: `tuna-credentials-{domain}` * **Account**: `{domain}` * **Data**: API token, account ID, domain (JSON) #### Security * Encrypted at rest by macOS * Requires biometric auth (Touch ID) or password to access * Never stored in files or environment variables * Not accessible to other applications ### Multiple Domains Run `--login` multiple times for different domains: ```bash tuna --login # Configure example.com tuna --login # Configure another-domain.com ``` Each domain's credentials are stored separately. ### Updating Credentials Run `--login` again to update credentials for a domain. The old credentials will be replaced. ### Troubleshooting #### "Invalid API token" * Check for typos when pasting * Ensure the token hasn't expired * Verify the token has all required permissions #### "Domain not found" * Ensure the domain is added to your Cloudflare account * Check that your token has access to that zone * Use just the root domain (e.g., `example.com`, not `www.example.com`) #### "Could not save to Keychain" * Check System Preferences → Security & Privacy → Privacy * Ensure your terminal app has Keychain access * Try running in a new terminal window ### See Also * [Installation](/guide/installation) - Full setup guide * [tuna --init](/commands/init) - Configure your project ## tuna \ The default command - wraps your dev server with a Cloudflare tunnel. ### Usage ```bash tuna [args...] ``` ### Examples ```bash # Node.js / npm scripts tuna npm run dev tuna npm start # Vite tuna vite dev --port 3000 # Next.js tuna next dev # Plain Node tuna node server.js # Python tuna python -m http.server 8080 # Any command tuna ./start-dev.sh ``` ### What It Does 1. **Reads configuration** from `package.json` 2. **Authenticates** with Cloudflare (biometric via Keychain) 3. **Creates tunnel** if it doesn't exist 4. **Sets up DNS** record pointing to tunnel 5. **Configures access control** if specified 6. **Starts cloudflared** as a background service 7. **Runs your command** with full stdio passthrough 8. **Exits** with your command's exit code ### Output ```bash $ tuna npm run dev 🔐 Touch ID required ✓ Retrieved credentials for example.com ✓ Tunnel ready (tuna-alice-api-example-com) ✓ DNS: alice-api.example.com → tunnel ✓ Service running 🌐 https://alice-api.example.com → localhost:3000 > my-app@1.0.0 dev > vite VITE v5.0.0 ready in 234 ms ➜ Local: http://localhost:3000/ ➜ Tunnel: https://alice-api.example.com/ ``` ### Behavior #### Stdio Passthrough All input and output passes through transparently: * **stdin** - Interactive prompts work * **stdout** - Colors and formatting preserved * **stderr** - Error output preserved * **TTY** - Terminal features work (progress bars, etc.) #### Signal Handling | Signal | Behavior | | ----------------- | -------------------------- | | `Ctrl+C` (SIGINT) | Forwarded to child process | | `SIGTERM` | Forwarded to child process | | `SIGHUP` | Forwarded to child process | The tunnel **continues running** after your command exits. This is intentional - it allows you to restart your dev server without recreating the tunnel. #### Exit Codes Tuna exits with the same code as your command: | Your Command | Tuna Exit Code | | ---------------- | -------------- | | Exits 0 | 0 | | Exits 1 | 1 | | Killed by signal | 128 + signal | | Ctrl+C | 130 | ### Configuration Required Your `package.json` must have a `tuna` section: ```json { "tuna": { "forward": "my-app.example.com", "port": 3000 } } ``` See [Configuration](/guide/configuration) for all options. ### Idempotent Running `tuna` multiple times is safe: * Tunnel is reused if it exists * DNS record is updated if needed * Service is restarted with new config * Access policies are synced ### Port Matching Make sure your `port` config matches your dev server: ```json { "tuna": { "port": 3000 // Must match! } } ``` ```bash tuna vite dev --port 3000 # ✅ Matches tuna vite dev --port 4000 # ❌ Won't work - tunnel points to 3000 ``` ### See Also * [Configuration](/guide/configuration) - All config options * [tuna --stop](/commands/stop) - Stop the tunnel service * [tuna --list](/commands/list) - View active tunnels ## tuna --stop Stop the cloudflared service. ### Usage ```bash tuna --stop ``` ### What It Does 1. Checks if the cloudflared service is running 2. Asks for confirmation 3. Stops the launchd service 4. All tunnels go offline ### Output ```bash $ tuna --stop ⚠ This will stop the cloudflared service and take all tunnels offline. ? Are you sure? Yes ✓ Cloudflared service stopped Tunnels are offline. Run 'tuna ' to restart. ``` ### Skip Confirmation Use `--yes` or `-y` to skip the confirmation prompt: ```bash tuna --stop --yes tuna --stop -y ``` ### Effects #### What Stops * The cloudflared daemon process * All active tunnel connections * All tunnels become inaccessible #### What Remains * Tunnel configurations (in Cloudflare) * DNS records * Credentials in Keychain * Local config files ### Restarting Simply run tuna again to restart: ```bash tuna npm run dev ``` The service will be reinstalled and tunnels will reconnect. ### When to Use #### Troubleshooting If tunnels aren't working correctly: ```bash tuna --stop tuna npm run dev # Fresh start ``` #### Freeing Resources The cloudflared service uses minimal resources, but you can stop it when not needed: ```bash tuna --stop # When done for the day ``` #### Before Uninstalling Stop the service before removing tuna: ```bash tuna --stop npm uninstall -g tuna ``` ### Service Not Running If the service is already stopped: ```bash $ tuna --stop Service is not running ``` This exits with code 0 (success). ### See Also * [tuna --delete](/commands/delete) - Remove tunnel completely * [tuna --list](/commands/list) - Check tunnel status ## Configuration Reference Complete reference for the `tuna` configuration in `package.json`. ### Schema ```json { "tuna": { "forward": string, // Required "port": number, // Required "access": string[] // Optional } } ``` ### Properties #### forward **Type:** `string`\ **Required:** Yes The domain where your tunnel will be accessible. ```json { "tuna": { "forward": "my-app.example.com" } } ``` **Validation:** * Must be a valid domain name * Must be a subdomain of a zone in your Cloudflare account * Applied after environment variable interpolation **Environment Variables:** | Variable | Description | Fallback | | ------------ | ----------------------- | --------- | | `$USER` | Current system username | `unknown` | | `$TUNA_USER` | Custom tuna identifier | `$USER` | | `$HOME` | Home directory path | `~` | **Examples:** ```json // Static domain { "forward": "api.example.com" } // Per-user subdomain { "forward": "$USER-api.example.com" } // alice → alice-api.example.com // Custom identifier { "forward": "$TUNA_USER-staging.example.com" } // TUNA_USER=v2 → v2-staging.example.com ``` *** #### port **Type:** `number`\ **Required:** Yes The local port to forward traffic to. ```json { "tuna": { "port": 3000 } } ``` **Validation:** * Must be an integer * Must be between 1 and 65535 **Common Ports:** | Framework | Default Port | | ---------------- | ------------ | | Vite | 5173 | | Next.js | 3000 | | Create React App | 3000 | | Remix | 3000 | | Express | 3000 | | Fastify | 3000 | | NestJS | 3000 | | FastAPI | 8000 | | Django | 8000 | | Rails | 3000 | *** #### access **Type:** `string[]`\ **Required:** No\ **Default:** `undefined` (public access) Array of email addresses or domains that can access the tunnel. ```json { "tuna": { "access": ["@company.com", "contractor@gmail.com"] } } ``` **Patterns:** | Pattern | Description | Example | | ------------------ | ---------------------------- | ----------------- | | `@domain.com` | Allow all emails from domain | `@company.com` | | `email@domain.com` | Allow specific email | `alice@gmail.com` | **Validation:** * Domain patterns must start with `@` * Email addresses must be valid format * Empty array `[]` is treated as no access control **Examples:** ```json // Public (no authentication) { "access": undefined } // or simply omit the field // Company only { "access": ["@mycompany.com"] } // Specific people { "access": ["alice@gmail.com", "bob@outlook.com"] } // Mixed { "access": ["@mycompany.com", "contractor@external.com"] } ``` *** ### Full Example ```json { "name": "my-api", "version": "1.0.0", "scripts": { "dev": "node server.js" }, "tuna": { "forward": "$USER-api.example.com", "port": 3000, "access": ["@mycompany.com", "client@clientcorp.com"] } } ``` ### Validation Errors #### Missing forward ``` Error: Missing "forward" in tuna config ``` #### Missing port ``` Error: Missing "port" in tuna config ``` #### Invalid port ``` Error: "port" must be a number Error: "port" must be between 1 and 65535 ``` #### Invalid domain ``` Error: Invalid domain format: $INVALID-app.example.com ``` This usually means an environment variable didn't resolve correctly. #### Unresolved variables ``` Error: Unresolved environment variables in forward field: $UNDEFINED-app.example.com Supported: $USER, $TUNA_USER, $HOME ``` ### TypeScript Types ```typescript interface TunaConfig { /** * Domain to expose. Supports environment variable interpolation. * @example "my-app.example.com" * @example "$USER-api.example.com" */ forward: string; /** * Local port to forward traffic to. * @example 3000 */ port: number; /** * Access control rules. Omit for public access. * @example ["@company.com", "user@gmail.com"] */ access?: string[]; } ``` ## Environment Variables Tuna supports environment variables for configuration and customization. ### Config Interpolation These variables can be used in the `forward` field of your tuna config: #### $USER The current system username. ```json { "tuna": { "forward": "$USER-api.example.com" } } ``` **Source:** `process.env.USER`\ **Fallback:** `unknown` **Example:** ```bash # User "alice" runs: tuna npm run dev # → alice-api.example.com ``` #### $TUNA\_USER Custom identifier that overrides `$USER`. ```json { "tuna": { "forward": "$TUNA_USER-api.example.com" } } ``` **Source:** `process.env.TUNA_USER`\ **Fallback:** `$USER` **Example:** ```bash # Override for a specific environment TUNA_USER=staging tuna npm run dev # → staging-api.example.com # Without override, falls back to $USER tuna npm run dev # → alice-api.example.com ``` #### $HOME User's home directory. Rarely used in domains. **Source:** `process.env.HOME`\ **Fallback:** `~` *** ### Runtime Environment Variables These variables affect tuna's behavior at runtime: #### TUNA\_API\_TOKEN Override Keychain credentials with an API token. ```bash export TUNA_API_TOKEN=your_cloudflare_token export TUNA_ACCOUNT_ID=your_account_id tuna npm run dev ``` **Use case:** CI/CD environments where Keychain isn't available. #### TUNA\_ACCOUNT\_ID Cloudflare account ID (used with `TUNA_API_TOKEN`). ```bash export TUNA_ACCOUNT_ID=699d98642c564d2e855e9661899b7252 ``` #### TUNA\_CLOUDFLARED\_PATH Custom path to cloudflared binary. ```bash export TUNA_CLOUDFLARED_PATH=/custom/path/cloudflared tuna npm run dev ``` **Default behavior:** Tuna looks for cloudflared in: 1. `PATH` 2. `~/.tuna/bin/cloudflared` #### TUNA\_HOME Override the tuna data directory. ```bash export TUNA_HOME=/custom/tuna/data tuna npm run dev ``` **Default:** `~/.tuna` **Contains:** * `tunnels/` - Tunnel credential files * `config-*.yml` - Cloudflared configs * `bin/` - Downloaded cloudflared binary *** ### Examples #### Team Development Each developer gets their own subdomain automatically: ```json { "tuna": { "forward": "$USER-dev.example.com", "port": 3000 } } ``` #### Feature Branches Use `TUNA_USER` for feature-specific tunnels: ```bash # Feature branch TUNA_USER=feature-auth tuna npm run dev # → feature-auth-dev.example.com # Main branch TUNA_USER=main tuna npm run dev # → main-dev.example.com ``` #### CI/CD Pipeline Use environment variables instead of Keychain: ```yaml # GitHub Actions - name: Start tunnel env: TUNA_API_TOKEN: ${{ secrets.CLOUDFLARE_TOKEN }} TUNA_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} TUNA_USER: ci-${{ github.run_id }} run: tuna npm run dev & ``` #### Custom cloudflared Use a specific version of cloudflared: ```bash export TUNA_CLOUDFLARED_PATH=/opt/cloudflared-2024.1.0 tuna npm run dev ``` *** ### Precedence For config interpolation: 1. `$TUNA_USER` (if set) 2. `$USER` (system username) 3. `unknown` (fallback) For credentials: 1. `TUNA_API_TOKEN` + `TUNA_ACCOUNT_ID` (if both set) 2. macOS Keychain (default)