When running ushadow behind Tailscale, you face a fundamental question: Who should be able to access shared links?
Your share links will look like:
https://YOUR_BASE_URL/share/a1b2c3d4-e5f6-7890-abcd-ef1234567890
But what should YOUR_BASE_URL be?
Best for: Sharing with colleagues/friends who are already on your Tailnet
Setup:
# In your .env file
SHARE_BASE_URL=https://ushadow.tail12345.ts.netHow it works:
- User clicks "Share" in conversation detail page
- Gets link like:
https://ushadow.tail12345.ts.net/share/{token} - Only people connected to your Tailnet can access
Implementation:
# In ushadow/backend/src/routers/share.py, implement _get_share_base_url():
def _get_share_base_url() -> str:
# Try explicit override first
if base_url := os.getenv("SHARE_BASE_URL"):
return base_url.rstrip("/")
# Use Tailscale hostname
try:
config = read_tailscale_config()
if config and config.hostname:
return f"https://{config.hostname}"
except Exception:
pass
# Fallback
return "http://localhost:3000"Pros:
- ✅ Simple - no extra infrastructure
- ✅ Secure - protected by Tailscale ACLs
- ✅ Works immediately
Cons:
- ❌ Recipients must join your Tailnet
- ❌ Not suitable for external friends
Best for: Sharing with external friends without deploying separate infrastructure
Setup:
# Enable Funnel for specific paths
tailscale funnel --bg --https=443 --set-path=/share https+insecure://localhost:8010
# In your .env file
SHARE_BASE_URL=https://ushadow.tail12345.ts.netHow it works:
- Tailscale Funnel exposes
/share/*endpoints publicly through Tailscale's infrastructure - Share links use your Tailscale hostname
- External users access via public internet → Tailscale Funnel → Your ushadow instance
Implementation: Same as Strategy 1 (Funnel is transparent to your app)
Pros:
- ✅ No separate VPS needed
- ✅ Tailscale handles SSL certificates
- ✅ Can selectively expose endpoints
Cons:
- ❌ Requires Tailscale Funnel configuration
- ❌ Funnel has bandwidth limits
- ❌ May not work with all Tailscale plans
Best for: Production deployments with external sharing and fine-grained control
Setup:
- Deploy
share-gateway/to a public VPS (e.g., DigitalOcean) - Configure gateway to proxy back to your Tailscale network
- Set environment variable:
# In your .env file
SHARE_PUBLIC_GATEWAY=https://share.yourdomain.comHow it works:
- User clicks "Share" in conversation
- Gets link like:
https://share.yourdomain.com/share/{token} - Gateway validates token with your ushadow backend via Tailscale
- Gateway proxies the conversation data back to external user
Implementation:
def _get_share_base_url() -> str:
# Public gateway for external sharing (highest priority)
if gateway_url := os.getenv("SHARE_PUBLIC_GATEWAY"):
return gateway_url.rstrip("/")
# Explicit override
if base_url := os.getenv("SHARE_BASE_URL"):
return base_url.rstrip("/")
# Fallback to Tailscale hostname
try:
config = read_tailscale_config()
if config and config.hostname:
return f"https://{config.hostname}"
except Exception:
pass
return "http://localhost:3000"Gateway Deployment:
cd share-gateway/
docker build -t ushadow-share-gateway .
docker run -d -p 443:8000 \
-e USHADOW_BACKEND_URL=https://ushadow.tail12345.ts.net \
-e RATE_LIMIT_PER_IP=10 \
ushadow-share-gatewayPros:
- ✅ Full control over public endpoint
- ✅ Custom domain and SSL
- ✅ Rate limiting and security controls
- ✅ No bandwidth limits
Cons:
- ❌ Requires deploying separate service
- ❌ Monthly VPS cost (~$5-10/month)
- ❌ More complex architecture
Here's the complete implementation for _get_share_base_url() in ushadow/backend/src/routers/share.py:
def _get_share_base_url() -> str:
"""Determine the base URL for share links.
Strategy hierarchy:
1. SHARE_BASE_URL environment variable (highest priority)
2. SHARE_PUBLIC_GATEWAY environment variable (for external sharing)
3. Tailscale hostname (for Tailnet-only sharing)
4. Fallback to localhost (development only)
Returns:
Base URL string (e.g., "https://ushadow.tail12345.ts.net")
"""
# Explicit override (for testing or custom deployments)
if base_url := os.getenv("SHARE_BASE_URL"):
logger.info(f"Using explicit SHARE_BASE_URL: {base_url}")
return base_url.rstrip("/")
# Public gateway for external sharing
if gateway_url := os.getenv("SHARE_PUBLIC_GATEWAY"):
logger.info(f"Using public gateway: {gateway_url}")
return gateway_url.rstrip("/")
# Use Tailscale hostname (works with or without Funnel)
try:
config = read_tailscale_config()
if config and config.hostname:
tailscale_url = f"https://{config.hostname}"
logger.info(f"Using Tailscale hostname: {tailscale_url}")
return tailscale_url
except Exception as e:
logger.warning(f"Failed to read Tailscale config: {e}")
# Fallback for development
logger.warning("Using localhost fallback - shares will only work locally!")
return "http://localhost:3000"For immediate Tailnet-only sharing:
# No configuration needed! Just use the Tailscale hostname detection
# Share links will automatically use: https://ushadow.tail{xxx}.ts.netTo override:
# Add to your .env file
SHARE_BASE_URL=https://your-custom-url.com- Start ushadow backend
- Check logs for:
Share service initialized with base_url: ... - Create a share link from conversation detail page
- Verify the URL format matches your expected base URL
- Protected by Tailscale ACLs
- No public exposure
- Requires recipients to join Tailnet
- Only
/share/*endpoints exposed - Still uses Tailscale authentication for admin features
- Funnel has rate limiting built-in
- Gateway validates all tokens before proxying
- Rate limiting per IP (default: 10 requests/minute)
- Admin endpoints still require Tailscale access
- Consider adding additional authentication for sensitive shares