# AppCrane - AI Agent Operations Guide

You are an AI agent with a deployment API key for an AppCrane server. You can create apps, deploy code, manage environment variables, configure health checks, and rollback -- all via curl.

## Connection

```bash
# Set these two values (provided by the admin)
export CC="https://crane.example.com"
export KEY="your_api_key_here"

# All requests use the X-API-Key header
# Example: curl -s -H "X-API-Key: $KEY" $CC/api/apps
```

## App Requirements

Every app managed by AppCrane must be a Node.js app (React frontend + Express backend). Here's what the agent needs to know when building or preparing an app for deployment.

### Required: deployhub.json manifest

Every app MUST have a `deployhub.json` in its root directory:

```json
{
  "name": "MyApp",
  "version": "1.0.0",
  "fe": {
    "build": "npm run build",
    "serve": "npx serve -s dist"
  },
  "be": {
    "entry": "node server.js",
    "health": "/api/health"
  },
  "data_dirs": ["data/"],
  "env_example": ".env.example"
}
```

### Required: Version management

The version is the single source of truth for what's deployed. It must be consistent across three places:

**1. `deployhub.json`** -- AppCrane reads this during deploy:
```json
{
  "name": "MyApp",
  "version": "1.2.0"
}
```

**2. Health endpoint** -- MUST return `version` in the JSON response. AppCrane reads this live to display the running version in the dashboard:
```javascript
// Read version from deployhub.json so it's always in sync
import { readFileSync } from 'fs';
const manifest = JSON.parse(readFileSync('./deployhub.json', 'utf8'));

app.get('/api/health', (req, res) => {
  res.json({ status: 'ok', version: manifest.version });
});
```

**3. Frontend UI** -- display the version somewhere visible (footer, settings, about):
```javascript
// Option A: Import at build time (Vite)
// In vite.config.js:
import { readFileSync } from 'fs';
const manifest = JSON.parse(readFileSync('./deployhub.json', 'utf8'));
export default defineConfig({
  define: { __APP_VERSION__: JSON.stringify(manifest.version) }
});
// In your React component:
<span>v{__APP_VERSION__}</span>

// Option B: Serve from backend API
app.get('/api/version', (req, res) => {
  res.json({ version: manifest.version, name: manifest.name });
});
// Frontend fetches /api/version on load and displays it
```

**Version bump workflow:**
1. Update `version` in `deployhub.json` (single place to change)
2. Commit and push
3. Deploy -- AppCrane picks up the new version automatically
4. Health endpoint and frontend both reflect the new version

**Never hardcode the version string in multiple files.** Always read from `deployhub.json` so there's one source of truth.

### Required: Health endpoint

The backend MUST expose a health check endpoint (default `/api/health`):

```javascript
import { readFileSync } from 'fs';
const manifest = JSON.parse(readFileSync('./deployhub.json', 'utf8'));

app.get('/api/health', (req, res) => {
  res.json({ status: 'ok', version: manifest.version });
});
```

AppCrane pings this endpoint every 30s. If it fails 3 times, the app auto-restarts. If it fails 5 times, the app is marked DOWN and an email alert is sent.

### Standard: Express server setup

Use the standard Node.js pattern for your server. AppCrane handles all port routing internally via Caddy -- your app is accessed by domain name, never by port:

```javascript
const app = express();
// ... your routes ...
app.listen(process.env.PORT || 3000);
```

### GitHub Access (Private Repos)

If the app repo is private, provide a GitHub Personal Access Token (PAT) when creating the app:

```bash
# Via curl (include github_token in the create app request)
curl -s -X POST $CC/api/apps -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"name":"MyApp","slug":"myapp","domain":"myapp.example.com","source_type":"github","github_url":"https://github.com/yourorg/private-repo","github_token":"ghp_your_token_here"}'
```

- The token is stored per-app (not global) and encrypted at rest (AES-256-GCM)
- The token is used only during `git clone` to pull the repo
- Generate a token at: https://github.com/settings/tokens (needs `repo` scope)
- Each app can have a different token (different repos, different orgs)
- Token can be updated later: `PUT /api/apps/SLUG` with `{"github_token":"ghp_new_token"}`

### Secrets and Environment Variables

- Secrets (API keys, database URLs, tokens) are NEVER stored in code or committed to git
- Set them via the AppCrane API as env vars: `PUT /api/apps/SLUG/env/ENV`
- AppCrane encrypts them at rest (AES-256-GCM) and writes them to `.env` at deploy time
- Access them in your app via `process.env.YOUR_VAR`
- Each environment (production/sandbox) has its own separate env vars
- ALWAYS use different database URLs for production and sandbox

Common env vars to set:
```
DATABASE_URL=postgres://user:pass@host:5432/dbname
API_KEY=sk-your-api-key
NODE_ENV=production (or development for sandbox)
SESSION_SECRET=random-string-here
SMTP_HOST=smtp.example.com
```

### Database

- AppCrane does NOT provision databases. The app must bring its own.
- Recommended: SQLite (simplest, file-based, stored in `/data/`) or external PostgreSQL
- If using SQLite, store the DB file in a `data/` directory so it persists across deploys and is included in backups
- Set the database path via env var: `DATABASE_URL=sqlite:./data/app.db`
- The `/data/` directory is symlinked across deploys -- it survives rollbacks and redeploys

### File structure expected by AppCrane

```
myapp/
  deployhub.json          ← REQUIRED manifest
  package.json            ← npm dependencies
  server.js               ← backend entry (or whatever be.entry says)
  data/                   ← persistent data (DB files, uploads) - survives deploys
  .env.example            ← template showing required env vars
  src/                    ← frontend source (if React app)
  dist/                   ← built frontend (after npm run build)
```

Or with separate frontend/backend directories:
```
myapp/
  deployhub.json
  frontend/
    package.json
    src/
    dist/
  backend/
    package.json
    server.js
  data/
  .env.example
```

### What happens during deploy

1. AppCrane pulls code from GitHub (or uses uploaded files)
2. Reads `deployhub.json` for build/start commands
3. Runs `npm install` (or `npm ci`)
4. Runs frontend build command (e.g., `npm run build`)
5. Writes `.env` file from encrypted env vars stored in AppCrane
6. Symlinks `data/` directory (persistent across deploys)
7. Starts backend via PM2 using the `be.entry` command
8. Runs health check on the health endpoint
9. If healthy: swaps the `current` symlink to new release
10. If unhealthy: marks deploy as failed, keeps previous version running

### What persists across deploys

| Persists | Does NOT persist |
|----------|-----------------|
| `/data/` directory (symlinked) | `node_modules/` (reinstalled each deploy) |
| `.env` file (written from AppCrane) | Source code (replaced each deploy) |
| Backup history | Build artifacts (rebuilt each deploy) |

### Fresh install checklist (for the agent)

When deploying a brand new app for the first time:

1. Ensure `deployhub.json` exists in repo root
2. Ensure `server.js` uses `app.listen(process.env.PORT || 3000)`
3. Ensure `/api/health` endpoint exists and returns 200
4. Ensure `package.json` has all dependencies
5. Ensure `.env.example` lists all required env vars
6. Push code to GitHub
7. Create the app on AppCrane (you are auto-assigned):
   ```bash
   curl -s -X POST $CC/api/apps \
     -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
     -d '{"name":"My App","slug":"myapp","domain":"myapp.example.com","source_type":"github","github_url":"https://github.com/yourorg/myapp"}'
   ```
8. Set env vars for sandbox: `PUT /api/apps/SLUG/env/sandbox`
9. Deploy to sandbox: `POST /api/apps/SLUG/deploy/sandbox`
10. Test health: `POST /api/apps/SLUG/health/sandbox/test`
11. Set env vars for production: `PUT /api/apps/SLUG/env/production`
12. Promote to production: `POST /api/apps/SLUG/promote`
13. Configure health check: `PUT /api/apps/SLUG/health/production`
14. Configure webhook for auto-deploy: `PUT /api/apps/SLUG/webhook`

---

## API Key Management

**IMPORTANT: API keys are shown ONLY ONCE at creation. They cannot be retrieved later.**

- Admin key is created during `crane init` (CLI on server). It's saved to `~/.appcrane/config.json`.
- User keys are created via `POST /api/users` and returned in the response. Save them immediately.
- If a key is lost, generate a new one: `POST /api/users/:id/regenerate-key` (admin only). The old key stops working.
- There is NO endpoint to retrieve an existing key. Keys are hashed (SHA-256) in the database.

```bash
# Create user and get key (admin)
curl -s -X POST $CC/api/users -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"name":"agent","email":"agent@example.com","role":"user"}'
# Response: { "api_key": "dhk_user_abc123...", "warning": "Save this key!" }

# Regenerate lost key (admin) - returns new key, old one is invalidated
curl -s -X POST $CC/api/users/2/regenerate-key -H "X-API-Key: $KEY"
# Response: { "api_key": "dhk_user_newkey...", "warning": "Save this key!" }
```

## Available API Endpoints

All endpoints require `X-API-Key` header unless noted.

```
GET    /api/info                           # Public, no auth
GET    /agent-guide                        # Public, no auth
GET    /docs                               # Public, no auth
GET    /login                              # Public, login page

# Identity (public, no X-API-Key needed)
POST   /api/identity/login                 # Login: { login, password, app? } → token
GET    /api/identity/verify                # Verify token: Authorization: Bearer TOKEN → user + role
GET    /api/identity/me                    # User profile from token
POST   /api/identity/logout                # Invalidate token

GET    /api/auth/me                        # Current API key user info
GET    /api/apps                           # List apps
POST   /api/apps                           # Create app (admin)
GET    /api/apps/:slug                     # App details
PUT    /api/apps/:slug                     # Update app (admin)
DELETE /api/apps/:slug?confirm=true        # Delete app (admin)
PUT    /api/apps/:slug/users               # Assign users (admin)

POST   /api/apps/:slug/deploy/:env         # Deploy (app user)
GET    /api/apps/:slug/deployments/:env    # Deploy history
GET    /api/apps/:slug/deployments/:env/:id/log  # Deploy log
POST   /api/apps/:slug/rollback/:env       # Rollback (app user)
POST   /api/apps/:slug/promote             # Promote sandbox→prod (app user)

GET    /api/apps/:slug/env/:env            # List env vars (app user)
PUT    /api/apps/:slug/env/:env            # Set env vars (app user)
DELETE /api/apps/:slug/env/:env/:key       # Delete env var (app user)

GET    /api/apps/:slug/health/:env         # Health config + state
PUT    /api/apps/:slug/health/:env         # Configure health (app user)
POST   /api/apps/:slug/health/:env/test    # Test health now

GET    /api/apps/:slug/webhook             # Webhook config
PUT    /api/apps/:slug/webhook             # Configure webhook (app user)

POST   /api/apps/:slug/backup/:env         # Create backup (app user)
GET    /api/apps/:slug/backups             # List backups
POST   /api/apps/:slug/restore/:id         # Restore backup (app user)
POST   /api/apps/:slug/copy-data           # Copy prod data→sandbox (app user)

GET    /api/apps/:slug/logs/:env           # App logs
GET    /api/apps/:slug/audit               # App audit log
GET    /api/audit                          # Global audit log (admin)

GET    /api/apps/:slug/metrics/:env        # App metrics
GET    /api/server/health                  # Server health (admin)

GET    /api/users                          # List users (admin)
POST   /api/users                          # Create user (admin)
DELETE /api/users/:id                      # Delete user (admin)
POST   /api/users/:id/regenerate-key       # New API key (admin)
PUT    /api/users/:id/password             # Set/change password (admin)
PUT    /api/users/:id/profile              # Update profile (admin)
PUT    /api/apps/:slug/roles               # Set per-app role (admin)
GET    /api/apps/:slug/identity/users      # List app users with roles (admin)

GET    /api/apps/:slug/notifications       # Notification config
PUT    /api/apps/:slug/notifications       # Configure notifications (app user)
POST   /api/apps/:slug/notifications/test  # Send test email (app user)

POST   /api/apps/:slug/upload/:env         # Upload app bundle (app user)
```

## Quick Reference

All commands use your API key: `-H "X-API-Key: $KEY"`

### App Management

| Action | Command |
|--------|---------|
| List my apps | `curl -s -H "X-API-Key: $KEY" $CC/api/apps` |
| Create app | `curl -s -X POST $CC/api/apps -H "X-API-Key: $KEY" -H "Content-Type: application/json" -d '{"name":"AppName","slug":"appslug","domain":"app.example.com","source_type":"github","github_url":"https://github.com/yourorg/repo"}'` |
| App details | `curl -s -H "X-API-Key: $KEY" $CC/api/apps/SLUG` |
| Delete app | `curl -s -X DELETE "$CC/api/apps/SLUG?confirm=true" -H "X-API-Key: $KEY"` |

### Deployment

| Action | Command |
|--------|---------|
| Deploy to sandbox | `curl -s -X POST $CC/api/apps/SLUG/deploy/sandbox -H "X-API-Key: $KEY"` |
| Deploy to production | `curl -s -X POST $CC/api/apps/SLUG/deploy/production -H "X-API-Key: $KEY"` |
| Promote sandbox to prod | `curl -s -X POST $CC/api/apps/SLUG/promote -H "X-API-Key: $KEY"` |
| Rollback | `curl -s -X POST $CC/api/apps/SLUG/rollback/production -H "X-API-Key: $KEY"` |
| Deploy history | `curl -s -H "X-API-Key: $KEY" $CC/api/apps/SLUG/deployments/ENV` |
| Deploy log | `curl -s -H "X-API-Key: $KEY" $CC/api/apps/SLUG/deployments/ENV/ID/log` |

### Environment Variables

| Action | Command |
|--------|---------|
| Set env vars | `curl -s -X PUT $CC/api/apps/SLUG/env/ENV -H "X-API-Key: $KEY" -H "Content-Type: application/json" -d '{"vars":{"DATABASE_URL":"...","API_KEY":"..."}}'` |
| List env vars | `curl -s -H "X-API-Key: $KEY" "$CC/api/apps/SLUG/env/ENV?reveal=true"` |
| Delete env var | `curl -s -X DELETE $CC/api/apps/SLUG/env/ENV/VARNAME -H "X-API-Key: $KEY"` |

### Health & Monitoring

| Action | Command |
|--------|---------|
| Health status | `curl -s -H "X-API-Key: $KEY" $CC/api/apps/SLUG/health/ENV` |
| Test health now | `curl -s -X POST $CC/api/apps/SLUG/health/ENV/test -H "X-API-Key: $KEY"` |
| Configure health | `curl -s -X PUT $CC/api/apps/SLUG/health/ENV -H "X-API-Key: $KEY" -H "Content-Type: application/json" -d '{"endpoint":"/api/health","interval_sec":30,"fail_threshold":3,"down_threshold":5}'` |
| Live version | `curl -s -H "X-API-Key: $KEY" $CC/api/apps/SLUG/live-version/ENV` |
| App logs | `curl -s -H "X-API-Key: $KEY" "$CC/api/apps/SLUG/logs/ENV?lines=100"` |

### Webhooks & Backups

| Action | Command |
|--------|---------|
| Webhook config | `curl -s -H "X-API-Key: $KEY" $CC/api/apps/SLUG/webhook` |
| Enable auto-deploy | `curl -s -X PUT $CC/api/apps/SLUG/webhook -H "X-API-Key: $KEY" -H "Content-Type: application/json" -d '{"auto_deploy_sandbox":true,"auto_deploy_prod":false}'` |
| Create backup | `curl -s -X POST $CC/api/apps/SLUG/backup/ENV -H "X-API-Key: $KEY"` |
| List backups | `curl -s -H "X-API-Key: $KEY" $CC/api/apps/SLUG/backups` |
| Restore backup | `curl -s -X POST $CC/api/apps/SLUG/restore/BACKUP_ID -H "X-API-Key: $KEY"` |

## Key Rules

1. **Your API key lets you create apps, deploy, and manage everything.** When you create an app, you are auto-assigned to it.
2. **Each app has TWO environments**: `production` and `sandbox`. Use ENV = `production` or `sandbox` in URLs.
3. **SLUG** is the app's URL-safe identifier (e.g., `bookclub`). Lowercase, alphanumeric with dashes.
4. **Promote copies CODE only** from sandbox to production. It never copies .env or /data/.
5. **Rollback** swaps to the previous deployment. Last 5 releases are kept.
6. **deployhub.json** manifest is required in each app's root directory.
7. **Env vars** are encrypted at rest.
8. **Ports are internal.** AppCrane assigns them automatically. Apps are accessed by domain, never by port.
9. **data/** directory persists across deploys. Store SQLite DBs and uploads there.
10. **Identity users are separate from deployment.** The `/login` and `/api/identity/*` endpoints are for app end-users who log into deployed apps, NOT for deployment operations. Your API key (X-API-Key) is all you need for deployment.
11. **node_modules/** is reinstalled on every deploy. Don't rely on local state in it.
12. **NEVER change a GitHub repository from private to public.** If the repo is private, keep it private. Use a GitHub token when creating the app to access private repos. Do not alter repository visibility settings.
13. **ALWAYS deploy to sandbox first.** Never deploy directly to production. The workflow is: deploy to sandbox → test → promote to production. This protects production from broken code.

## Typical Workflow (for an AI agent)

You have an API key. Here's the full flow to build and deploy an app:

1. Build your app (React frontend + Express backend)
2. Add `deployhub.json` to the root (see manifest section above)
3. Add `/api/health` endpoint that returns `{ status: 'ok', version: '1.0.0' }`
4. Push to GitHub
5. Create the app on AppCrane (auto-assigns you):
   `POST /api/apps` with `{ "name": "My App", "slug": "myapp", "domain": "myapp.example.com", "source_type": "github", "github_url": "https://github.com/..." }`
6. Set sandbox env vars → `PUT /api/apps/myapp/env/sandbox`
7. Deploy to sandbox → `POST /api/apps/myapp/deploy/sandbox`
8. Test health → `POST /api/apps/myapp/health/sandbox/test`
9. Set prod env vars → `PUT /api/apps/myapp/env/production`
10. Promote to production → `POST /api/apps/myapp/promote`
11. If broken, rollback → `POST /api/apps/myapp/rollback/production`

**You do NOT need admin help.** Your API key can create apps, deploy, manage env vars, and monitor health.

## Troubleshooting

| Problem | Check | Fix |
|---------|-------|-----|
| Deploy fails at npm install | Missing dependencies in package.json | Add missing packages, push, redeploy |
| Deploy fails at build | Build command wrong in deployhub.json | Fix `fe.build` in deployhub.json |
| Health check fails | App not starting correctly | Check deploy log, ensure `app.listen(process.env.PORT \|\| 3000)` |
| Health check 404 | Wrong health endpoint | Fix `be.health` in deployhub.json or `PUT /health/ENV` config |
| App crashes after deploy | Check deploy log | `GET /api/apps/SLUG/deployments/ENV/ID/log` |
| Env var missing | Not set for this environment | `PUT /api/apps/SLUG/env/ENV` with the missing var |
| Database lost after deploy | DB file not in data/ directory | Move SQLite to `data/app.db`, update DATABASE_URL |
| Can't deploy (403) | Wrong API key or not assigned | Check with admin, use app user key not admin key |

## Identity Manager

AppCrane acts as a central identity provider for all managed apps. Users log in once at AppCrane and get access to all their assigned apps.

### How apps integrate identity

1. User visits your app → app checks for session cookie
2. No cookie → redirect to AppCrane login:
   ```
   https://crane.example.com/login?app=YOUR_SLUG&redirect=https://yourapp.example.com/auth/callback
   ```
3. User logs in at AppCrane
4. AppCrane redirects back: `https://yourapp.example.com/auth/callback?token=SESSION_TOKEN`
5. App stores token in cookie, then verifies on each request:
   ```bash
   curl -s -H "Authorization: Bearer SESSION_TOKEN" \
     $CC/api/identity/verify?app=YOUR_SLUG
   ```
6. Response:
   ```json
   {
     "user": { "id": 1, "name": "Sarah", "email": "sarah@example.com", "username": "sarah", "avatar_url": null, "phone": null, "year_of_birth": 1990 },
     "role": "admin",
     "app": "bookclub"
   }
   ```

### Identity endpoints (no X-API-Key needed)

```bash
# Login (returns token + user + apps list)
curl -s -X POST $CC/api/identity/login \
  -H "Content-Type: application/json" \
  -d '{"login":"sarah@example.com","password":"xxx","app":"bookclub"}'

# Verify token (app calls this to check user)
curl -s -H "Authorization: Bearer TOKEN" "$CC/api/identity/verify?app=bookclub"

# Get user profile from token
curl -s -H "Authorization: Bearer TOKEN" $CC/api/identity/me

# Logout
curl -s -X POST $CC/api/identity/logout -H "Authorization: Bearer TOKEN"
```

### Admin: manage identity users

```bash
# Create user with password
curl -s -X POST $CC/api/users -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"name":"Sarah","email":"sarah@example.com","username":"sarah","password":"temp123","phone":"+1234567890","year_of_birth":1990}'

# Set/change password
curl -s -X PUT $CC/api/users/2/password -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"password":"newpass123"}'

# Update profile
curl -s -X PUT $CC/api/users/2/profile -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"avatar_url":"https://example.com/avatar.jpg","phone":"+1234567890"}'

# Set per-app role (admin/user/viewer)
curl -s -X PUT $CC/api/apps/bookclub/roles -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"user_id":2,"app_role":"admin"}'

# List users + roles for an app
curl -s -H "X-API-Key: $KEY" $CC/api/apps/bookclub/identity/users
```

### App roles
- **admin** -- full access within the app
- **user** -- standard access
- **viewer** -- read-only access

Roles are per-app. A user can be admin on BookClub but viewer on another app.

### User fields available to apps
| Field | Type | Description |
|-------|------|-------------|
| id | int | Stable user ID across all apps |
| name | string | Display name |
| email | string | Email address |
| username | string | Login username |
| avatar_url | string | Profile picture URL |
| phone | string | Phone number |
| year_of_birth | int | Year of birth |
| role | string | Role for THIS app (admin/user/viewer) |

### Implementing auth in your app (code examples)

**Backend (Express) -- add these routes to your server:**

```javascript
// auth.js -- add to your Express app

const CRANE_URL = process.env.CRANE_URL || 'https://crane.example.com';
const APP_SLUG = process.env.APP_SLUG || 'myapp';
const APP_URL = process.env.APP_URL || 'https://myapp.example.com';

// Middleware: check if user is authenticated
function requireUser(req, res, next) {
  const token = req.headers.authorization?.replace('Bearer ', '') || req.cookies?.cc_token;
  if (!token) {
    // API request: return 401
    if (req.path.startsWith('/api/')) {
      return res.status(401).json({ error: 'Not authenticated' });
    }
    // Browser request: redirect to AppCrane login
    return res.redirect(`${CRANE_URL}/login?app=${APP_SLUG}&redirect=${APP_URL}/auth/callback`);
  }

  // Verify token with AppCrane
  fetch(`${CRANE_URL}/api/identity/verify?app=${APP_SLUG}`, {
    headers: { 'Authorization': 'Bearer ' + token }
  })
    .then(r => r.json())
    .then(data => {
      if (data.error) {
        res.clearCookie('cc_token');
        return res.redirect(`${CRANE_URL}/login?app=${APP_SLUG}&redirect=${APP_URL}/auth/callback`);
      }
      req.user = data.user;      // { id, name, email, username, avatar_url, phone, year_of_birth }
      req.userRole = data.role;   // 'admin', 'user', or 'viewer'
      next();
    })
    .catch(() => res.status(500).json({ error: 'Auth service unavailable' }));
}

// Optional: restrict to certain roles
function requireRole(...roles) {
  return (req, res, next) => {
    if (!roles.includes(req.userRole)) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
    next();
  };
}

// Callback route: AppCrane redirects here after login
app.get('/auth/callback', (req, res) => {
  const token = req.query.token;
  if (!token) return res.redirect('/');
  // Set cookie (httpOnly, secure in production)
  res.cookie('cc_token', token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    maxAge: 24 * 60 * 60 * 1000  // 24 hours
  });
  res.redirect('/');
});

// Logout
app.get('/auth/logout', (req, res) => {
  const token = req.cookies?.cc_token;
  if (token) {
    // Tell AppCrane to invalidate the session
    fetch(`${CRANE_URL}/api/identity/logout`, {
      method: 'POST',
      headers: { 'Authorization': 'Bearer ' + token }
    }).catch(() => {});
  }
  res.clearCookie('cc_token');
  res.redirect('/');
});

// API: get current user (for frontend to fetch)
app.get('/api/me', requireUser, (req, res) => {
  res.json({ user: req.user, role: req.userRole });
});

// Example: protected route
app.get('/api/admin/settings', requireUser, requireRole('admin'), (req, res) => {
  res.json({ settings: '...' });
});

// Use requireUser on all protected routes
app.use('/api/data', requireUser);
```

**Frontend (React) -- check auth and show user:**

```javascript
// useAuth.js -- React hook for authentication
import { useState, useEffect } from 'react';

export function useAuth() {
  const [user, setUser] = useState(null);
  const [role, setRole] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch('/api/me')
      .then(r => r.ok ? r.json() : Promise.reject())
      .then(data => { setUser(data.user); setRole(data.role); })
      .catch(() => { setUser(null); setRole(null); })
      .finally(() => setLoading(false));
  }, []);

  return {
    user,           // { id, name, email, avatar_url, ... } or null
    role,           // 'admin', 'user', 'viewer', or null
    loading,
    isAuthenticated: !!user,
    isAdmin: role === 'admin',
    login: () => window.location.href = '/auth/callback?redirect=' + window.location.pathname,
    logout: () => window.location.href = '/auth/logout',
  };
}

// App.jsx -- use it
function App() {
  const { user, role, loading, isAuthenticated, logout } = useAuth();

  if (loading) return <div>Loading...</div>;
  if (!isAuthenticated) return null; // backend redirects to AppCrane login

  return (
    <div>
      <header>
        <span>Welcome {user.name}</span>
        <span>Role: {role}</span>
        {user.avatar_url && <img src={user.avatar_url} alt="" />}
        <button onClick={logout}>Logout</button>
      </header>
      {/* Your app content */}
    </div>
  );
}
```

**Required env vars for your app:**
```
CRANE_URL=https://crane.example.com
APP_SLUG=myapp
APP_URL=https://myapp.example.com
```

Set these via AppCrane: `PUT /api/apps/myapp/env/production`

## Full API docs with examples

Visit: `$CC/docs` (e.g., https://crane.example.com/docs)
