/
Documentation

Authentication

How session tokens, scopes, and the permission system work under the hood.

Session Token Flow

When a user opens your add-on, SyncBooks creates a short-lived JWT session token:

text
1. User clicks your addon in SyncBooks
2. SyncBooks server creates a JWT with:
   - orgId: the organization
   - userId: the current user
   - appId: your addon ID
   - scopes: permissions granted during installation
   - exp: 1 hour from now
3. Token is passed to your iframe via URL param: ?syncbooks_token=eyJ...
4. SyncBooksProvider extracts the token automatically
5. All API calls include: Authorization: Bearer <token>
6. Backend validates JWT on every request

Token Structure (Decoded JWT)

JWT Payload
{
  "type": "addon_session",
  "orgId": "507f1f77bcf86cd799439011",
  "userId": "507f1f77bcf86cd799439012",
  "appId": "com.mycompany.myapp",
  "scopes": ["invoices.read", "customers.read", "customers.write"],
  "jti": "a1b2c3d4-unique-id",
  "iat": 1691920000,
  "exp": 1691923600  // 1 hour later
}

Tokens expire after 1 hour

After expiry, API calls return 401. The user needs to close and reopen the add-on to get a fresh token. Handle this gracefully in your UI.

Scope Enforcement

Every API endpoint checks the token's scopes before returning data:

text
GET /api/addon/v1/customers
  → addonSessionAuth: validates JWT ✓
  → requireAddonScope('customers.read'): checks scopes array ✓
  → Returns data

POST /api/addon/v1/customers
  → addonSessionAuth: validates JWT ✓
  → requireAddonScope('customers.write'): checks scopes array
  → If 'customers.write' not in scopes: 403 Forbidden

Scope Format

// Specific scope
"customers.read"      // Can list/get customers
"customers.write"     // Can create/update customers
"customers.delete"    // Can delete customers

// Wildcard scope
"customers.*"         // All customer operations
"*"                   // All operations (full access)

Handling 401 (Token Expired)

App.tsx
import { useSyncBooks } from '@syncbooks/addon-sdk'

function App() {
  const { ready } = useSyncBooks()
  const [expired, setExpired] = useState(false)

  // Catch API errors globally
  useEffect(() => {
    const handler = (e: any) => {
      if (e?.status === 401) setExpired(true)
    }
    window.addEventListener('unhandledrejection', handler)
    return () => window.removeEventListener('unhandledrejection', handler)
  }, [])

  if (expired) {
    return (
      <div className="p-6 text-center">
        <p className="font-medium">Session Expired</p>
        <p className="text-sm text-muted-foreground mt-1">
          Please close and reopen this add-on to continue.
        </p>
        <button
          onClick={() => window.location.reload()}
          className="mt-3 px-4 py-2 bg-emerald-600 text-white text-sm rounded-lg"
        >
          Reload
        </button>
      </div>
    )
  }

  return <MainContent />
}

Handling 403 (Insufficient Scopes)

import { customers, formatError, hasScope, ui } from '@syncbooks/addon-sdk'

// Check before attempting
if (!hasScope('customers.write')) {
  ui.notify('You need "customers.write" permission for this action', 'warning')
  return
}

// Or handle the error
try {
  await customers.create({ name: 'Test' })
} catch (error: any) {
  if (error.status === 403) {
    // error.response = { requiredScope: 'customers.write', grantedScopes: [...] }
    ui.notify(`Missing permission: ${error.response.requiredScope}`, 'error')
  }
}

Security Best Practices

  • Never expose tokens to end users — don't display them in the UI
  • Don't store tokens — they're short-lived and auto-provided
  • Check scopes before showing UI — use PermissionGate or hasScope()
  • Handle errors gracefully — 401 and 403 are expected, not crashes
  • HTTPS only — your add-on URL must be HTTPS in production

Next Steps