# Teachfloor Docs
> Complete documentation for Large Language Models
---
## Document: Teachfloor Developer Documentation
URL: /home
# Teachfloor Developer Documentation
Build powerful integrations with the Teachfloor platform. Explore our guides, API reference, and SDKs to get started.
---
### Guides
---
## Document: /docs/README
URL: /docs/README
# Teachfloor Docs
This repo is the **source of truth** for public documentation shown on **[docs.teachfloor.com](https://docs.teachfloor.com)**.
---
## Document: /docs/api
URL: /docs/api
# Teachfloor API
REST API for integrating with the Teachfloor platform.
## Overview
The Teachfloor Public API provides programmatic access to your organization's data, enabling you to build integrations, automate workflows, and extend the platform's functionality.
**Base URL:** `https://api.teachfloor.com`
## Key Features
- **RESTful design** - Standard HTTP methods (GET, POST, PUT, DELETE)
- **JSON responses** - Easy to parse and work with
- **Resource-oriented** - Predictable URL structure
- **Comprehensive** - Access courses, members, activities, and more
## Quick Links
- [Authentication](./api/authentication) - Get your API key
- [Rate Limiting](./api/rate-limiting) - Usage limits
- [Errors](./api/errors) - Error handling
- API Reference - Interactive documentation at [docs.teachfloor.com](https://docs.teachfloor.com/api-reference)
## Getting Started
1. Generate an API key from your Teachfloor dashboard
2. Include it in the `Authorization` header
3. Make requests to the API endpoints
```bash
curl https://api.teachfloor.com/v0/members \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Available Resources
- **Activities** - Retrieve activity completion data
- **Courses** - Manage courses and enrollments
- **Members** - Access member information
- **Custom Fields** - Work with custom field data
See the full [API Reference](https://docs.teachfloor.com/api-reference) for complete endpoint documentation.
## Need Help?
Contact support through your Teachfloor dashboard.
---
## Document: /docs/api/authentication
URL: /docs/api/authentication
# Authentication
All API requests require authentication using an API key.
## How It Works
Include your API key in the `Authorization` header using Bearer authentication:
```
Authorization: Bearer YOUR_API_KEY
```
## Getting Your API Key
1. Log in to your Teachfloor account
2. Navigate to **Settings** → **Integrations**
3. Click **Generate API Key**
4. Copy and store the key securely
**Security**: Never share your API key publicly or commit it to version control.
## Example Request
```bash
curl https://api.teachfloor.com/v0/members \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"
```
```javascript
const response = await fetch('https://api.teachfloor.com/v0/members', {
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
}
});
```
```php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.teachfloor.com/v0/members",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer YOUR_API_KEY",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
```
## Regenerating Your Key
You can regenerate your API key at any time:
1. Go to **Settings** → **Integrations**
2. Click **Regenerate API Key**
**Warning**: Regenerating invalidates the previous key. Update all applications using the old key.
## Best Practices
- Store API keys in environment variables
- Never hardcode keys in your application
- Regenerate immediately if compromised
## Next Steps
- [Rate Limiting](./rate-limiting) - Understand usage limits
- [Errors](./errors) - Handle API errors
---
## Document: /docs/api/rate-limiting
URL: /docs/api/rate-limiting
# Rate Limiting
The API enforces rate limits to ensure platform stability and fair usage.
## Rate Limit
**50 requests per minute** per API key.
## Rate Limit Headers
Each API response includes headers showing your current rate limit status:
| Header | Description |
|--------|-------------|
| `X-RateLimit-Limit` | Maximum requests per minute (50) |
| `X-RateLimit-Remaining` | Requests remaining in current minute |
## Example Response Headers
```
X-RateLimit-Limit: 50
X-RateLimit-Remaining: 42
```
## Exceeding the Limit
If you exceed the rate limit, you'll receive:
**Status Code:** `429 Too Many Requests`
**Response:**
```json
{
"error": "Rate limit exceeded. Please try again later."
}
```
## Best Practices
### Monitor Remaining Requests
```javascript
const response = await fetch('https://api.teachfloor.com/v0/members', {
headers: { 'Authorization': 'Bearer YOUR_API_KEY' }
});
const remaining = response.headers.get('X-RateLimit-Remaining');
console.log(`Requests remaining: ${remaining}`);
if (remaining < 5) {
// Slow down requests
}
```
### Implement Backoff
```javascript
async function makeRequest(url) {
try {
const response = await fetch(url, {
headers: { 'Authorization': 'Bearer YOUR_API_KEY' }
});
if (response.status === 429) {
// Wait 60 seconds and retry
await new Promise(resolve => setTimeout(resolve, 60000));
return makeRequest(url);
}
return response;
} catch (error) {
console.error('Request failed:', error);
}
}
```
### Batch Requests
Optimize your API usage:
- Cache responses when possible
- Combine related data fetches
- Use webhooks for real-time updates instead of polling
## Need Higher Limits?
Contact support through your Teachfloor dashboard to discuss your use case.
## Next Steps
- [Errors](./errors) - Handle API errors
- [Authentication](./authentication) - API key setup
---
## Document: /docs/api/errors
URL: /docs/api/errors
# Errors
Understanding and handling API errors.
## Error Response Format
All errors return JSON with an `error` field:
```json
{
"error": "Error message describing what went wrong"
}
```
## HTTP Status Codes
| Code | Meaning | Description |
|------|---------|-------------|
| `200` | OK | Request successful |
| `400` | Bad Request | Invalid request parameters |
| `401` | Unauthorized | Invalid or missing API key |
| `403` | Forbidden | Authenticated but not authorized |
| `404` | Not Found | Resource doesn't exist |
| `429` | Too Many Requests | Rate limit exceeded |
| `500` | Internal Server Error | Server error |
## Common Errors
### Invalid API Key
**Status:** `401 Unauthorized`
```json
{
"error": "Unauthorized"
}
```
**Solution:** Check your API key is correct and included in the `Authorization` header.
### Resource Not Found
**Status:** `404 Not Found`
```json
{
"error": "Resource not found"
}
```
**Solution:** Verify the resource ID exists and you have access to it.
### Rate Limit Exceeded
**Status:** `429 Too Many Requests`
```json
{
"error": "Rate limit exceeded. Please try again later."
}
```
**Solution:** Wait 60 seconds before retrying. See [Rate Limiting](./rate-limiting).
### Invalid Parameters
**Status:** `400 Bad Request`
```json
{
"error": "Invalid parameters provided"
}
```
**Solution:** Check your request parameters match the API documentation.
## Next Steps
- [Authentication](./authentication) - API key setup
- [Rate Limiting](./rate-limiting) - Usage limits
---
## Document: /docs/webhooks
URL: /docs/webhooks
# Teachfloor Webhooks
Real-time event notifications from the Teachfloor platform.
## What Are Webhooks?
Teachfloor uses webhooks to notify your application in real-time whenever specific events occur within your account. Instead of continuously polling the API for updates, webhooks push notifications directly to your system as events happen.
## How Webhooks Work
When a subscribed event occurs, Teachfloor sends an HTTP POST request to your designated webhook endpoint with a JSON payload containing detailed information about the event.
### Common Use Cases
- **Sync your database** - Keep records updated with course enrollments, completions
- **Send notifications** - Alert users about important events
- **Trigger workflows** - Automate processes based on platform events
## Quick Links
- [Getting Started](./webhooks/getting-started) - Set up your first webhook
- [Security](./webhooks/security) - Verify webhook signatures
- [Delivery & Retries](./webhooks/delivery-retries) - Understanding webhook reliability
- [Event Reference](./webhooks/event-reference) - Available webhook events
- [Troubleshooting](./webhooks/troubleshooting) - Common issues and solutions
## Need Help?
If you have questions or need assistance, contact support through your Teachfloor dashboard.
---
## Document: /docs/webhooks/getting-started
URL: /docs/webhooks/getting-started
# Getting Started
Set up webhooks to receive real-time event notifications from Teachfloor.
## Prerequisites
- Teachfloor account with access to Developers page
- Publicly accessible HTTPS endpoint
- Ability to process HTTP POST requests
## Setup Steps
### 1. Access Webhook Settings
1. Log in to your Teachfloor account
2. Navigate to **Developers** → **Webhooks**
### 2. Add Endpoint
1. Click **Add Endpoint**
2. Enter your HTTPS endpoint URL
3. Select which events to receive
4. Click **Save**
### 3. Get Signing Secret
1. Click **Reveal Signing Secret** on your endpoint
2. Copy and store the secret securely
3. Use this to verify webhook signatures
## Webhook Payload
All webhooks have this structure:
```json
{
"id": "evt_abc123",
"type": "course.join",
"created_at": "2025-10-07T15:30:00.000000Z",
"data": {
// Event-specific data
}
}
```
## Basic Implementation
```javascript
app.post('/webhooks/teachfloor', express.json(), (req, res) => {
// Respond immediately
res.status(200).send('OK');
// Process async
processWebhook(req.body).catch(console.error);
});
```
```php
Route::post('/webhooks/teachfloor', function (Request $request) {
// Respond immediately
response('OK', 200)->send();
// Process async
ProcessWebhookJob::dispatch($request->json()->all());
});
```
## Requirements
Your endpoint must:
- Accept POST requests
- Return 2xx status within 10 seconds
- Verify signatures (see [Security](./security))
## Next Steps
- [Security](./security) - Verify signatures
- [Delivery & Retries](./delivery-retries) - Understand reliability
- [Event Reference](./event-reference) - View available events
- [Troubleshooting](./troubleshooting) - Common issues
---
## Document: /docs/webhooks/security
URL: /docs/webhooks/security
# Security
Verify that webhook events are genuinely sent by Teachfloor.
## Why Verify?
Signature verification ensures:
- Events are genuinely from Teachfloor
- Payloads haven't been tampered with
- Protection against malicious requests
**Always verify signatures in production.**
## How It Works
Teachfloor signs each webhook using HMAC-SHA256:
```
signature = hash_hmac('sha256', json_payload, secret)
```
The signature is sent in the `Teachfloor-Signature` header.
## Getting Your Secret
1. Go to **Developers** → **Webhooks**
2. Select your endpoint
3. Click **Reveal Signing Secret**
4. Store securely (never commit to version control)
## Implementation
### PHP
```php
function verifyWebhookSignature(Request $request, string $secret): bool
{
$payload = $request->getContent();
$signature = $request->headers->get('Teachfloor-Signature');
$generated = hash_hmac('sha256', $payload, $secret);
return hash_equals($generated, $signature);
}
Route::post('/webhooks/teachfloor', function (Request $request) {
$secret = config('services.teachfloor.webhook_secret');
if (!verifyWebhookSignature($request, $secret)) {
return response('Invalid signature', 401);
}
ProcessWebhookJob::dispatch($request->json()->all());
return response('OK', 200);
});
```
### Node.js
```javascript
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const generated = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(generated),
Buffer.from(signature)
);
}
app.post('/webhooks/teachfloor',
express.raw({ type: 'application/json' }),
(req, res) => {
const secret = process.env.TEACHFLOOR_WEBHOOK_SECRET;
const signature = req.headers['teachfloor-signature'];
const payload = req.body.toString('utf8');
if (!verifyWebhookSignature(payload, signature, secret)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(payload);
processWebhookEvent(event);
res.status(200).send('OK');
}
);
```
## Important Notes
- Use raw request body (not parsed JSON)
- Use timing-safe comparison (`hash_equals` or `timingSafeEqual`)
- Store secrets in environment variables
- Verify before processing
## Next Steps
- [Delivery & Retries](./delivery-retries)
- [Event Reference](./event-reference)
- [Troubleshooting](./troubleshooting)
---
## Document: /docs/webhooks/delivery-retries
URL: /docs/webhooks/delivery-retries
# Delivery & Retries
Understanding webhook reliability and retry behavior.
## How Delivery Works
1. Event occurs in Teachfloor
2. HTTP POST sent to your endpoint (10 second timeout)
3. Response checked
4. 2xx status = success, otherwise retry
## Successful Delivery
Your endpoint must respond with 2xx status within 10 seconds:
- `200 OK` (recommended)
- `201 Created`
- `202 Accepted`
- `204 No Content`
## Retry Mechanism
- **Total attempts**: 3 (initial + 2 retries)
- **Timeout**: 10 seconds per attempt
- **Backoff**: Exponential (10^attempt seconds)
### Retry Schedule
| Attempt | Wait Time | Total Elapsed |
|---------|-----------|---------------|
| Initial | 0s | 0s |
| 1st retry | 10s | 10s |
| 2nd retry | 100s | 110s |
### When Retries Occur
- Non-2xx status code
- Timeout (>10 seconds)
- Connection failure
## Handling Duplicates
Every delivery carries a `Teachfloor-Idempotency-Key` header whose value is stable across retry attempts (it equals the envelope `id` field). Use it as your dedupe key — checking the header lets you short-circuit before parsing the body:
```javascript
const processed = new Set();
function processWebhook(req) {
const key = req.headers['teachfloor-idempotency-key']; // or event.id from the body
if (processed.has(key)) return;
processed.add(key);
// Process event
}
```
Retries carry the SAME key, so any 2xx you send after a retry-triggering timeout will still be treated as duplicate work on your side.
## Monitoring
View delivery logs in your Teachfloor dashboard:
1. Go to **Developers** → **Webhooks**
2. Select your endpoint
3. Check delivery history
## Next Steps
- [Event Reference](./event-reference)
- [Security](./security)
- [Troubleshooting](./troubleshooting)
---
## Document: /docs/webhooks/event-reference
URL: /docs/webhooks/event-reference
# Event Reference
Complete reference of available webhook events.
## Event Structure
All webhook events follow this JSON structure:
```json
{
"id": "evt_abc123",
"type": "course.join",
"created_at": "2025-10-07T15:30:00.000000Z",
"data": {
// Event-specific data
}
}
```
### Root Fields
| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Unique identifier for this webhook event |
| `type` | string | The event type (e.g., `course.join`, `element.completed`) |
| `created_at` | string | ISO 8601 timestamp when the event was created |
| `data` | object | Event-specific payload (structure varies by event type) |
## Available Events
### Course Events
#### `course.created`
Triggered when a new course is created.
#### `course.updated`
Triggered when a course is updated.
#### `course.completed`
Triggered when a member completes a course.
#### `course.join`
Triggered when a member joins a course.
**Payload example:**
```json
{
"id": "evt_abc123",
"type": "course.join",
"created_at": "2025-10-07T15:30:00.000000Z",
"data": {
"id": "cm_xyz789",
"object": "course_member",
"joined_at": "2025-10-07T15:30:00.000000Z",
"member": {
"id": "usr_123",
"object": "member",
"first_name": "John",
"last_name": "Doe",
"full_name": "John Doe",
"avatar": "https://example.com/avatar.jpg",
"email": "john@example.com",
"is_email_verified": true,
"last_seen": "2025-10-07T14:00:00.000000Z"
},
"course": {
"id": "crs_456",
"object": "course",
"created_at": "2025-01-01T00:00:00.000000Z",
"name": "Introduction to Development",
"status": "published",
"cover": "https://example.com/cover.jpg",
"availability": "public",
"visibility": "listed",
"start_date": null,
"end_date": null,
"currency": "USD",
"price": 99.00,
"free_label": null,
"url": "https://app.teachfloor.com/org/intro-dev",
"public_url": "https://app.teachfloor.com/org/intro-dev/join",
"join_url": "https://app.teachfloor.com/org/intro-dev/join",
"metadata": {},
"custom_fields": {}
},
"custom_fields": {}
}
}
```
### Module Events
#### `module.created`
Triggered when a new module is created.
#### `module.updated`
Triggered when a module is updated.
### Element Events
#### `element.created`
Triggered when a new element is created.
#### `element.updated`
Triggered when an element is updated.
#### `element.deleted`
Triggered when an element is deleted.
#### `element.completed`
Triggered when a member completes an element.
**Payload example:**
```json
{
"id": "evt_def456",
"type": "element.completed",
"created_at": "2025-10-07T15:30:00.000000Z",
"data": {
"id": "act_789",
"object": "activity",
"timestamp": "2025-10-07T15:30:00.000000Z",
"status": "completed",
"passed": true,
"score": 95,
"completed_by": "usr_123",
"member": {
"id": "usr_123",
"object": "member",
"first_name": "John",
"last_name": "Doe",
"full_name": "John Doe",
"avatar": "https://example.com/avatar.jpg",
"email": "john@example.com",
"is_email_verified": true,
"last_seen": "2025-10-07T15:30:00.000000Z"
},
"context": {
"id": "elm_321",
"object": "element",
"created_at": "2025-01-01T00:00:00.000000Z",
"name": "Introduction Video",
"cover": "https://example.com/thumb.jpg",
"type": "video",
"position": 1,
"module": "mod_654",
"metadata": {}
}
}
}
```
### Member Events
#### `member.login`
Triggered when a member logs in.
## Object Types
### Member Object
```json
{
"id": "usr_123",
"object": "member",
"first_name": "John",
"last_name": "Doe",
"full_name": "John Doe",
"avatar": "https://example.com/avatar.jpg",
"email": "john@example.com",
"is_email_verified": true,
"last_seen": "2025-10-07T15:30:00.000000Z"
}
```
### Course Object
```json
{
"id": "crs_456",
"object": "course",
"created_at": "2025-01-01T00:00:00.000000Z",
"name": "Course Name",
"status": "published",
"cover": "https://example.com/cover.jpg",
"availability": "public",
"visibility": "listed",
"start_date": null,
"end_date": null,
"currency": "USD",
"price": 99.00,
"free_label": null,
"url": "https://app.teachfloor.com/org/course-slug",
"public_url": "https://app.teachfloor.com/org/course-slug/join",
"join_url": "https://app.teachfloor.com/org/course-slug/join",
"metadata": {},
"custom_fields": {}
}
```
### Element Object
```json
{
"id": "elm_321",
"object": "element",
"created_at": "2025-01-01T00:00:00.000000Z",
"name": "Element Name",
"cover": "https://example.com/thumb.jpg",
"type": "video",
"position": 1,
"module": "mod_654",
"metadata": {}
}
```
## Related Documentation
- [Getting Started](./getting-started) - Set up your first webhook
- [Security](./security) - Verify webhook signatures
- [Delivery & Retries](./delivery-retries) - Understand reliability
---
## Document: /docs/webhooks/troubleshooting
URL: /docs/webhooks/troubleshooting
# Troubleshooting
Common issues and solutions for webhooks.
## Not Receiving Webhooks
**Check:**
- Endpoint URL is correct and accessible
- SSL certificate is valid
- Events are selected in webhook settings
- Firewall allows incoming HTTPS traffic
- Endpoint returns 2xx status code
**Test your endpoint:**
```bash
curl -X POST https://your-endpoint.com/webhooks/teachfloor \
-H "Content-Type: application/json" \
-d '{"test": true}'
```
## High Retry Rate
**Causes:**
- Endpoint not responding with 2xx status
- Response taking longer than 10 seconds
- Application errors during processing
- Infrastructure issues
**Solutions:**
- Respond with 2xx immediately (before processing)
- Move processing to background jobs
- Fix application errors
- Check server logs for issues
## Signature Verification Fails
**Check:**
1. Using correct secret for this endpoint
2. Using raw request body (not parsed JSON)
3. No middleware modifying the body
4. Correct character encoding (UTF-8)
**Debug:**
```javascript
console.log('Received signature:', req.headers['teachfloor-signature']);
console.log('Payload length:', payload.length);
console.log('Generated signature:', generatedSignature);
```
## Timeouts
**Causes:**
- Synchronous processing before responding
- Slow database queries
- External API calls before sending response
**Solution:**
```javascript
// Correct - respond immediately
app.post('/webhooks', (req, res) => {
res.status(200).send('OK');
processAsync(req.body);
});
// Wrong - may timeout
app.post('/webhooks', async (req, res) => {
await processEvent(req.body);
res.status(200).send('OK');
});
```
## Duplicate Events
**Cause:**
Webhooks may be delivered more than once due to retries.
**Solution:**
Use `event.id` for deduplication:
```javascript
const processed = new Set();
function processWebhook(event) {
if (processed.has(event.id)) return;
processed.add(event.id);
// Process event
}
```
## Viewing Delivery Logs
Check webhook delivery history:
1. Go to **Developers** → **Webhooks**
2. Select your endpoint
3. View delivery log
The log shows:
- Timestamp of each attempt
- Response status code
- Response time
- Error details
## Need Help?
Contact support through your Teachfloor dashboard.
---
## Document: /docs/apps
URL: /docs/apps
# Teachfloor Apps
Welcome to the Teachfloor App Development documentation! This guide will help you build powerful extensions that integrate seamlessly with the Teachfloor platform.
## What are Teachfloor Apps?
Teachfloor Apps are extensions that enhance the platform's functionality by adding custom user interfaces, integrations, and workflows. Apps can:
- Display custom UI components in various locations throughout the dashboard
- Store and retrieve data securely
- Access platform resources through permissions
- Integrate with external services
- Enhance the learning experience for instructors and learners
## Documentation Structure
### Getting Started
1. [Introduction](./apps/introduction) - Overview and concepts
2. [Quickstart Guide](./apps/quickstart) - Build your first app in 10 minutes
### Core Concepts
3. [App Manifest](./apps/core-concepts/app-manifest) - Configure your app's metadata and capabilities
4. [Viewports System](./apps/core-concepts/viewports) - Understand where apps can display
5. [Surfaces](./apps/core-concepts/surfaces) - How your app renders — drawer or widget — and how it pairs with viewports
6. [Extension Kit Components](./apps/core-concepts/extension-kit/components) - Use pre-built UI components
7. [Extension Kit Integration](./apps/core-concepts/extension-kit/integration) - Interact with the platform
### Advanced Topics
8. [Data Storage](./apps/advanced-topics/data-storage) - Persist user and app data
9. [Realtime Channels](./apps/advanced-topics/realtime) - Subscribe to and publish low-latency events between learners
10. [Webhooks](./apps/advanced-topics/webhooks) - Receive signed HTTP deliveries to your app's backend when platform events fire
11. [OAuth](./apps/advanced-topics/oauth) - Call the public API from your app's backend on behalf of the installing organization
12. [Permissions](./apps/advanced-topics/permissions) - Request and use platform resources
13. [Deployment](./apps/advanced-topics/deployment) - Deploy private apps and publish to marketplace
### Reference
14. [CLI Commands](./apps/references/cli) - Complete command reference
15. [Best Practices](./apps/references/best-practices) - Tips and patterns
16. [Examples](./apps/references/examples) - Sample apps and code snippets
17. [Troubleshooting](./apps/references/troubleshooting) - Common issues and solutions
## Prerequisites
Before you begin, ensure you have:
- Node.js 18.0.0 or higher
- npm or yarn package manager
- A Teachfloor account
- Basic knowledge of React and JavaScript
## Support
- **Issues**: [GitHub Issues](https://github.com/teachfloor/docs/issues)
- **Email**: support@teachfloor.com
## License
This documentation is provided under the MIT License.
---
**Ready to get started?** Begin with the [Introduction](./apps/introduction) or jump straight to the [Quickstart Guide](./apps/quickstart)!
---
## Document: /docs/apps/introduction
URL: /docs/apps/introduction
# Introduction to Teachfloor Apps
Teachfloor Apps are powerful extensions that allow you to customize and enhance the Teachfloor learning platform. Built using modern web technologies, apps integrate seamlessly into the platform's interface while maintaining security and performance.
## What Can You Build?
### UI Extensions
Add custom interface components throughout the Teachfloor dashboard:
- Sidebar widgets for quick access
- Course-specific tools
- Custom settings pages
- Analytics dashboards
- Productivity tools
### Data Applications
Create apps that store and manage data:
- Note-taking apps
- Task managers
- Progress trackers
- Custom reports
### Backend Integrations (No UI Required)
Build apps that run in the background without any user interface:
- Event tracking to analytics platforms (Segment, Mixpanel)
- User activity sync to CRM systems (Intercom, HubSpot)
- Learning data export to data warehouses
- Automated notifications and webhooks
- Third-party API integrations
### Full-Stack Integrations
Combine UI and backend capabilities:
- Communication tools (Slack, Discord)
- AI assistants with chat interfaces
- Custom analytics with dashboards
- Content recommendation engines
## Architecture
### Three Core Components
```
┌─────────────────────────────────────────────────────┐
│ Teachfloor CLI │
│ Command-line tool for app development & deployment │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ Extension Kit Library │
│ React components & utilities for building UIs │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ Teachfloor Platform │
│ Hosts apps, manages permissions, serves SDK │
└─────────────────────────────────────────────────────┘
```
### How Apps Run
Apps run in sandboxed environments within the Teachfloor dashboard:
1. **Isolation**: Each app runs in its own sandbox for security
2. **Communication**: Apps communicate with the platform via PostMessage RPC
3. **Context**: Platform provides user, organization, and environment data
4. **Storage**: Secure data persistence through the Extension Kit
```
┌──────────────────────────────────────────────┐
│ Teachfloor Dashboard (Parent) │
│ ┌────────────────────────────────────────┐ │
│ │ Your App (Sandboxed) │ │
│ │ │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ React Components │ │ │
│ │ │ (Extension Kit) │ │ │
│ │ └──────────────────────────────┘ │ │
│ │ ↕ │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ Teachfloor SDK │ │ │
│ │ │ (PostMessage RPC) │ │ │
│ │ └──────────────────────────────┘ │ │
│ └────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
```
## Key Concepts
### App Manifest
The manifest is a JSON configuration file that defines your app:
```json
{
"id": "unique-app-id",
"version": "1.0.0",
"name": "My Awesome App",
"description": "Enhance your workflow",
"distribution_type": "public",
"ui_extension": {
"views": [
{
"viewport": "teachfloor.dashboard.course.list",
"component": "CourseListView"
}
]
},
"permissions": [
{
"permission": "courses:read",
"purpose": "Display course information"
}
]
}
```
### Viewports
Viewports define where your app can display within the platform. Each viewport corresponds to a specific page or section, such as course pages, settings, community areas, and more.
:::info
See [Viewports System](./core-concepts/viewports) for the complete list of available viewports and their usage.
:::
### Extension Context
Apps receive real-time context from the platform:
```javascript
{
userContext: {
id: "user-123",
full_name: "John Doe",
email: "john@example.com",
avatar: "https://...",
language: "en",
timezone: "America/New_York"
},
appContext: {
id: "app-456",
name: "My Awesome App"
},
environment: {
initialized: true,
viewport: "teachfloor.dashboard.course.list",
path: "/org/courses"
}
}
```
### Permissions
Apps must request permissions to access platform resources:
- `user:read`: Read user information
- `courses:read`: Access course data
- `modules:read`: Access module content
- `elements:read`: Access learning elements
- `user_events:read`: Track user activity
## Development Workflow
```
1. Install CLI
↓
2. Authenticate
↓
3. Create App
↓
4. Add Views & Components
↓
5. Request Permissions
↓
6. Test Locally
↓
7. Build & Upload
↓
8. Submit for Review
↓
9. Publish to Marketplace
```
## Distribution Models
### Private Apps
- Only visible to your organization
- Perfect for internal tools
- No review required for use
- Instant deployment
### Public Apps
- Listed in the Teachfloor Marketplace
- Available to all organizations
- Requires review and approval
- Potential for monetization
## Technology Stack
### Required
- **React 18+**: UI framework
- **JavaScript/TypeScript**: Programming languages
- **Webpack**: Module bundler (configured by CLI)
### Provided
- **Extension Kit**: Pre-built components library
- **Teachfloor SDK**: Platform integration API
- **CLI Tools**: Development and deployment utilities
### Optional
- Any React-compatible libraries
- State management (Redux, Zustand, etc.)
- Styling solutions (Emotion, Tailwind, etc.)
- Additional build tools
## Security & Privacy
### Sandboxing
- Apps run in isolated sandboxes
- Cannot access parent page directly
- All communication through SDK
### Permissions
- Explicit permission declarations in manifest
- Granular access control
- Transparent to users during installation
### Data Storage
- Scoped to app and organization
- Encrypted at rest
- GDPR compliant
### Review Process
- Code quality checks
- Security vulnerability scanning
- Privacy policy requirements
- Terms of service compliance
## Limitations & Constraints
### Technical
- Maximum 6 apps per organization
- 10 MB bundle size limit (recommended)
- 1000 API calls per minute (rate limiting)
- 10 MB data storage per user
### Platform
- Apps cannot modify core platform UI
- Limited to defined viewports
- Cannot run code on Teachfloor's servers (your own backend runs on your infrastructure — see [OAuth](./oauth) and [Webhooks](./webhooks))
- No direct database access
## Next Steps
Now that you understand the basics, let's build your first app!
→ Continue to [Quickstart Guide](./quickstart)
---
## Document: /docs/apps/quickstart
URL: /docs/apps/quickstart
# Quickstart Guide
Build your first Teachfloor app in 10 minutes! This guide walks you through creating a simple "Hello World" app that displays in the course list page.
## Prerequisites
- Node.js 18.0+ installed
- npm or yarn package manager
- A Teachfloor account
## Setup Steps
### 1. Install the CLI
Open your terminal and install the Teachfloor CLI globally:
```bash
npm install -g @teachfloor/teachfloor-cli
```
Verify the installation:
```bash
teachfloor version
```
### 2. Authenticate
Log in to your Teachfloor account:
```bash
teachfloor login
```
You'll be prompted for:
- **Email**: Your Teachfloor account email
- **Password**: Your account password
- **Organization**: Select your organization (if you have multiple)
Once authenticated, verify your session:
```bash
teachfloor whoami
```
### 3. Create Your First App
Create a new app called "hello-world":
```bash
teachfloor apps create hello-world
```
You'll be prompted for:
- **App ID**: Press Enter to accept the default (auto-generated)
- **Display Name**: Enter "Hello World"
- **Description**: Enter "My first Teachfloor app"
- **Version**: Press Enter to accept "1.0.0"
The CLI will:
1. Create the app on the platform
2. Generate the project structure
3. Install dependencies (React, Extension Kit, etc.)
```
✓ Creating app...
✓ Setting up app structure...
✓ Installing npm dependencies...
✓ App "Hello World" created successfully in "hello-world".
```
### 4. Explore the Project Structure
Navigate into your app directory:
```bash
cd hello-world
```
Your project structure looks like this:
```
hello-world/
├── src/
│ ├── index.js # Entry point
│ └── views/
│ └── App.jsx # Main view component
├── public/
│ └── index.html # HTML template with SDK
├── teachfloor-app.json # App manifest
├── package.json # Dependencies
├── webpack.config.js # Webpack config
├── tsconfig.json # TypeScript config
└── .gitignore
```
### 5. Add a View
Add a view to display your app in the course list page:
```bash
teachfloor apps add view
```
Select:
- **Viewport**: `teachfloor.dashboard.course.list`
- **Component Name**: Press Enter to accept "CourseListView"
- **Generate example**: Yes
This creates `src/views/CourseListView.jsx` and updates your manifest.
### 6. Customize Your Component
Open `src/views/CourseListView.jsx` and modify it:
```jsx
import React from 'react'
import {
Container,
Text,
Button,
SimpleGrid,
showToast,
useExtensionContext,
} from '@teachfloor/extension-kit'
const CourseListView = () => {
const { userContext } = useExtensionContext()
const handleClick = () => {
showToast('Hello from your app!', { color: 'green' })
}
return (
Hello, {userContext.full_name}!
Welcome to your first Teachfloor app.
)
}
export default CourseListView
```
### 7. Start the Development Server
Run the development server:
```bash
teachfloor apps start
```
This will:
1. Validate your manifest
2. Upload the manifest to the platform
3. Open your browser to install the app
4. Start the webpack dev server on `http://localhost:3000`
```
✓ Manifest file updated
Install URL: https://app.teachfloor.com/your-org/courses?app=abc123@1.0.0
Starting development server...
```
### 8. Test Your App
1. Your browser will open to the install URL
2. Click "Install" to add the app to your organization
3. Navigate to the course list page
4. You should see your app displayed!
The app will hot-reload as you make changes to the code.
### 9. Build for Production
When you're ready to deploy:
1. Stop the dev server (Ctrl+C)
2. Build the production bundle:
```bash
teachfloor apps upload
```
This will:
1. Run `npm run build`
2. Bundle your app for production
3. Upload files to the platform
4. Create a new app version
```
✓ Building the production bundle...
✓ Uploading files...
✓ App uploaded successfully.
```
### 10. Publish to Marketplace (Optional)
If you want to make your app available in the public marketplace:
**Set Distribution to Public**
First, set your app's distribution type to public:
```bash
teachfloor apps set distribution
```
Select `public` when prompted. This updates your manifest:
```json
{
"distribution_type": "public"
}
```
**Upload and Submit**
1. Upload your app with the updated manifest:
```bash
teachfloor apps upload
```
2. Go to your Teachfloor dashboard
3. Navigate to Settings → Apps
4. Find your app and click "Submit for Review"
5. Wait for approval from the Teachfloor team
:::caution
Only apps with `distribution_type: "public"` can be submitted for marketplace review. Private apps (`distribution_type: "private"`) are only visible to your organization and don't require review.
:::
## What's Next?
Congratulations! You've built your first Teachfloor app. Here's what to explore next:
### Learn Core Concepts
- [App Manifest](./core-concepts/app-manifest) - Configure your app
- [Viewports](./core-concepts/viewports) - Understand placement options
- [Extension Kit](./core-concepts/extension-kit/components) - Explore available components
### Add More Functionality
- [Data Storage](./advanced-topics/data-storage) - Persist user data
- [Permissions](./advanced-topics/permissions) - Access platform resources
- [Extension Kit Integration](./core-concepts/extension-kit/integration) - Use platform features
### See Examples
- [Example Apps](./references/examples) - Sample code and patterns
---
**Ready to dive deeper?** Continue to [App Manifest](./core-concepts/app-manifest)
---
## Document: /docs/apps/core-concepts/app-manifest
URL: /docs/apps/core-concepts/app-manifest
# App Manifest
The app manifest (`teachfloor-app.json`) defines your app's metadata, capabilities, permissions, and behavior.
## Overview
The manifest is a JSON file at the root of your project:
- Defines app name, description, and version
- Specifies where your app displays (viewports)
- Declares required permissions
- Sets distribution type (public/private)
- Configures post-installation actions
## Basic Structure
Here's a minimal manifest:
```json
{
"id": "abc123def456",
"version": "1.0.0",
"name": "My App",
"description": "A simple Teachfloor app",
"distribution_type": "private"
}
```
## Complete Manifest Example
```json
{
"id": "abc123def456",
"version": "1.2.0",
"name": "Course Notes",
"description": "Take notes while browsing courses and modules",
"distribution_type": "public",
"ui_extension": {
"views": [
{
"viewport": "teachfloor.dashboard.course.detail",
"component": "CourseNotesView"
},
{
"viewport": "teachfloor.dashboard.course.module.detail",
"component": "ModuleNotesView"
}
],
"permissions_policy": {
"microphone": ["http://localhost:3000"],
"purpose": "Enable speech-to-text for note taking"
}
},
"permissions": [
{
"permission": "user:read",
"purpose": "Display your name and profile"
},
{
"permission": "courses:read",
"purpose": "Display course information in notes"
}
],
"post_install_action": {
"type": "settings",
"url": "https://example.com/setup"
}
}
```
## Field Reference
### Required Fields
#### `id` (string)
Unique identifier for your app. Auto-generated during app creation.
```json
"id": "abc123def456"
```
**Rules:**
- Must be unique across all Teachfloor apps
- Cannot be changed after creation
- Automatically generated by CLI
#### `version` (string)
Semantic version of your app following [semver](https://semver.org/) format.
```json
"version": "1.2.3"
```
**Rules:**
- Must follow `MAJOR.MINOR.PATCH` format
- Examples: `1.0.0`, `2.1.5`, `0.5.0-beta`
- Increment when uploading new versions
**Versioning Guidelines:**
- **MAJOR**: Breaking changes
- **MINOR**: New features, backward compatible
- **PATCH**: Bug fixes, backward compatible
#### `name` (string)
Display name shown to users in the marketplace and dashboard.
```json
"name": "Course Notes"
```
**Rules:**
- Maximum 50 characters
- Should be descriptive and unique
- Used in app listings and UI
#### `description` (string)
Short description of what your app does.
```json
"description": "Take notes while browsing courses and modules"
```
**Rules:**
- Maximum 200 characters
- Should clearly explain the app's purpose
- Displayed in marketplace listings
### Distribution
#### `distribution_type` (string)
Determines how your app is distributed.
```json
"distribution_type": "public"
```
**Values:**
- `"private"`: Only your organization can install (default)
- `"public"`: Listed in marketplace for all organizations
:::info
See [Deployment Guide](./advanced-topics/deployment#distribution-types) for detailed information on private vs public distribution.
:::
### UI Extension
#### `ui_extension` (object, optional)
Defines where and how your app displays in the platform.
:::info
The `ui_extension` field is optional. Apps without it can function as backend integrations listening to platform events.
:::
##### `views` (array)
List of views your app provides.
```json
"ui_extension": {
"views": [
{
"viewport": "teachfloor.dashboard.course.list",
"component": "CourseListView"
},
{
"viewport": "teachfloor.dashboard.course.detail",
"component": "CourseDetailView"
}
]
}
```
**View Object:**
- `viewport` (string): Where the view displays (must be an exact match to an available viewport)
- `component` (string): React component name (must match filename without extension)
:::caution
Viewports require exact string matches. Wildcard patterns are not supported. See [Viewports System](./viewports) for available viewports.
:::
##### `permissions_policy` (object)
Browser permissions your app needs (microphone, camera, etc.).
```json
"ui_extension": {
"permissions_policy": {
"microphone": ["http://localhost:3000", "https://app.teachfloor.com"],
"camera": ["http://localhost:3000"],
"purpose": "Enable video recording for assignments"
}
}
```
**Available Permissions:**
- `microphone`: Audio input
- `camera`: Video input
- `geolocation`: Location access
- `clipboard-write`: Clipboard access
**Fields:**
- Permission name: Array of allowed origins
- `purpose`: User-facing explanation (required)
### Permissions
#### `permissions` (array)
Platform permissions your app needs to access Teachfloor resources.
```json
"permissions": [
{
"permission": "courses:read",
"purpose": "Display course information in widgets"
},
{
"permission": "user_events:read",
"purpose": "Track learning progress"
}
]
```
**Permission Object:**
- `permission` (string): Permission identifier
- `purpose` (string): User-facing explanation of why you need it
**Important**:
- Only request permissions you actually use
- Provide clear, user-facing explanations
- Users see permission requests during installation
- Write permissions (`*:write`) automatically include read access
:::info
See [Permissions Reference](./advanced-topics/permissions) for the complete list of available permissions, their descriptions, and permission hierarchy.
:::
### Post-Install Action
#### `post_install_action` (object)
Defines what happens after a user installs your app.
```json
"post_install_action": {
"type": "settings"
}
```
**Type: Settings**
Redirect to app settings page:
```json
{
"type": "settings"
}
```
**Type: External**
Redirect to external URL:
```json
{
"type": "external",
"url": "https://example.com/setup?user=123"
}
```
**Use Cases:**
- Configuration wizard
- Account connection
- Welcome tutorial
- Feature introduction
## Manifest Validation
The CLI automatically validates your manifest before upload.
### Validation Rules
#### Required Fields
- `id`, `version`, `name` must be present
#### Version Format
- Must match semver pattern: `^\d+\.\d+\.\d+(-[\w.]+)?$`
- Valid: `1.0.0`, `2.1.5`, `1.0.0-beta.1`
- Invalid: `1.0`, `v1.0.0`, `1.0.0.0`
#### Component Names
- Must be valid React component names
- PascalCase only
- Valid: `MyView`, `CourseDetailView`
- Invalid: `myView`, `my-view`, `my_view`
#### Viewport Names
- Must match known viewport patterns
- See [Viewports documentation](./viewports)
### Manual Validation
The manifest is automatically validated when you run:
```bash
teachfloor apps start
# or
teachfloor apps upload
```
## Managing Manifests
### Updating Metadata
#### Via CLI Commands
Update distribution type:
```bash
teachfloor apps set distribution
```
Add permission:
```bash
teachfloor apps grant permission
```
Remove permission:
```bash
teachfloor apps revoke permission
```
Add view:
```bash
teachfloor apps add view
```
Remove view:
```bash
teachfloor apps remove view
```
#### Manual Editing
1. Open `teachfloor-app.json` in your editor
2. Make changes
3. Save the file
4. Run `teachfloor apps start` or `teachfloor apps upload`
The CLI will validate and upload the updated manifest.
### Version Management
Once a version is published, it becomes locked and cannot be modified. To release updates, increment the version number and upload again.
:::info
See [Deployment Guide](./advanced-topics/deployment#version-management) for complete information on version states, semantic versioning, and deployment process.
:::
### Multi-Environment Manifests
Use different manifests for development and production:
**Development** (`teachfloor-app.dev.json`):
```json
{
"id": "abc123",
"version": "1.0.0-dev",
"name": "My App (Dev)",
"distribution_type": "private",
"ui_extension": {
"permissions_policy": {
"microphone": ["http://localhost:3000"]
}
}
}
```
**Production** (`teachfloor-app.json`):
```json
{
"id": "abc123",
"version": "1.0.0",
"name": "My App",
"distribution_type": "public",
"ui_extension": {
"permissions_policy": {
"microphone": ["https://app.teachfloor.com"]
}
}
}
```
Start with custom manifest:
```bash
teachfloor apps start --manifest teachfloor-app.dev.json
```
## Next Steps
→ Continue to [Viewports System](./viewports)
## Additional Resources
- [Best Practices](./references/best-practices) - Naming, descriptions, permissions, and versioning guidelines
- [Troubleshooting Guide](./references/troubleshooting) - Common manifest errors and solutions
- [Examples](./references/examples) - Complete manifest examples
- [Permissions Reference](./advanced-topics/permissions)
- [CLI Commands](./references/cli)
---
## Document: /docs/apps/core-concepts/viewports
URL: /docs/apps/core-concepts/viewports
# Viewports System
Viewports define where your app displays within the Teachfloor platform.
Viewport is one of two axes that fully describe where and how your app renders. The other axis — [surface](./surfaces) — answers *how* your app is presented (as a drawer that opens from the app dock, or as a widget embedded inline). Together, `(viewport, surface)` uniquely specifies each entry in your manifest.
## What is a Viewport?
A viewport is a specific location in the Teachfloor dashboard where your app renders its UI. Each viewport corresponds to a page or section of the platform.
### Viewport Structure
Viewports follow a hierarchical naming pattern:
```
teachfloor.{surface}.{resource}.{view-type}
```
**Example**: `teachfloor.dashboard.course.detail`
- `teachfloor`: Platform namespace
- `dashboard`: Surface where the view renders
- `course`: Resource being viewed
- `detail`: View type
## Available Viewports
### Quick Reference Table
| Viewport | Page | Path Pattern | Common Use Cases |
|----------|------|--------------|------------------|
| **Course Management** |
| `teachfloor.dashboard.course.list` | Course listing | `/:org/courses` | Course directory widgets, filters, quick actions |
| `teachfloor.dashboard.course.detail` | Course detail | `/:org/courses/:id` | Course analytics, notes, supplementary materials |
| `teachfloor.dashboard.course.module.list` | Module list | `/:org/courses/:id/modules` | Module overview, navigation tools |
| `teachfloor.dashboard.course.module.detail` | Module detail | `/:org/courses/:id/modules/:id` | Module tools, notes, progress tracking |
| `teachfloor.dashboard.course.element.detail` | Element detail | `/:org/courses/:id/modules/:id/elements/:id` | Element annotations, supplementary content |
| `teachfloor.dashboard.course.assessment.list` | Assessment list | `/:org/courses/:id/assessments` | Assessment overview, quiz management |
| `teachfloor.dashboard.course.progress.detail` | Course progress | `/:org/courses/:id/progress` | Progress tracking, completion status |
| `teachfloor.dashboard.course.calendar.detail` | Course calendar | `/:org/courses/:id/calendar` | Schedule view, deadline tracking |
| `teachfloor.dashboard.course.member.list` | Course members | `/:org/courses/:id/directory` | Student list, member management |
| **Community** |
| `teachfloor.dashboard.community.overview` | Community home (cross-channel feed) | `/:org/community` | Cross-channel feed widgets, suggested content, digest tools |
| `teachfloor.dashboard.community.channel.detail` | Single channel page | `/:org/community/:channelId` | Social features, engagement tools, moderation |
| `teachfloor.dashboard.community.post.detail` | Community post detail | `/:org/community/:channelId/posts/:postId` | Post sidebars, reaction tools, translation |
| `teachfloor.dashboard.community.member.list` | Community members | `/:org/community/:channelId/members` | Member profiles, networking tools, search |
| `teachfloor.dashboard.community.event.list` | Community events | `/:org/community/events` | Calendar exports, RSVP tooling, scheduling helpers |
| `teachfloor.dashboard.community.event.detail` | Community event detail | `/:org/community/events/:eventId` | Attendee tools, meeting integrations, reminders |
| **Settings** |
| `teachfloor.dashboard.settings.general.detail` | General settings | `/:org/settings/general` | Organization-wide integrations, preferences |
| `teachfloor.dashboard.settings.customization.domain.detail` | Customization · Domain | `/:org/settings/customization/domain` | Domain configuration widgets, DNS helpers |
| `teachfloor.dashboard.settings.customization.appearance.detail` | Customization · Appearance | `/:org/settings/customization/appearance` | Theme tools, brand previews |
| `teachfloor.dashboard.settings.customization.smtp.detail` | Customization · SMTP | `/:org/settings/customization/smtp` | Email-delivery diagnostics |
| `teachfloor.dashboard.settings.customization.checkout.detail` | Customization · Checkout | `/:org/settings/customization/checkout` | Checkout experiments, payment tweaks |
| `teachfloor.dashboard.settings.customization.login.detail` | Customization · Login | `/:org/settings/customization/login` | Login-screen branding |
| `teachfloor.dashboard.settings.customization.labels.detail` | Customization · Labels | `/:org/settings/customization/labels` | Custom terminology tools |
| `teachfloor.dashboard.settings.customization.achievements.detail` | Customization · Achievements | `/:org/settings/customization/achievements` | Gamification configuration |
| `teachfloor.dashboard.settings.customization.profile-layout.detail` | Customization · Profile layout | `/:org/settings/customization/profile-layout` | Profile field arrangement tools |
| `teachfloor.dashboard.settings.customization.dashboards.detail` | Customization · Dashboards | `/:org/settings/customization/dashboards` | Home-dashboard widget catalog, layout authoring tools |
| `teachfloor.dashboard.settings.team.list` | Team management | `/:org/settings/team` | Team collaboration tools, role management |
| `teachfloor.dashboard.settings.billing.detail` | Billing | `/:org/settings/billing` | Usage analytics, cost tracking |
| `teachfloor.dashboard.settings.integration.list` | Integrations | `/:org/settings/integrations` | Third-party integrations, API tools |
| `teachfloor.dashboard.settings.notification.list` | Notifications | `/:org/settings/notifications` | Custom notification rules, digest tools |
| `teachfloor.dashboard.settings.custom-field.list` | Custom fields | `/:org/settings/custom-fields` | Custom field management, data import tools |
| `teachfloor.dashboard.settings.branch.list` | Branches | `/:org/settings/branches` | Branch management, multi-tenant tools |
| `teachfloor.dashboard.settings.sanction.list` | Sanctions | `/:org/settings/sanctions` | Restriction policies, moderation tooling |
| `teachfloor.dashboard.settings.import.list` | Import data | `/:org/settings/import` | Bulk import tools, data migration |
| `teachfloor.dashboard.settings.app.list` | Apps | `/:org/settings/apps` | Installed apps, app marketplace |
| **User Management** |
| `teachfloor.dashboard.account.detail` | User account | `/:org/account` | Personal tools, user preferences, integrations |
| `teachfloor.dashboard.profile.detail` | User profile | `/:org/users/:userId` | Profile widgets, member insights |
| `teachfloor.dashboard.learner.list` | Learners | `/:org/learners` | Learner analytics, bulk actions, import tools |
| **Commerce** |
| `teachfloor.dashboard.payment.list` | Payments | `/:org/payments` | Payment analytics, invoicing tools |
| **Messaging** |
| `teachfloor.dashboard.messaging.thread.detail` | Thread | `/:org/messaging/threads/:thread` | Thread sidebars, translation tools, message-level integrations |
| **Workspace** |
| `teachfloor.dashboard.dashboard.detail` | Home dashboard | `/:org/dashboard` | Widget-based home insights, custom KPIs, welcome banners |
| `teachfloor.dashboard.analytics.detail` | Analytics | `/:org/analytics` | Custom dashboards, exports, alerts |
| `teachfloor.dashboard.library.detail` | Library drive | `/:org/library/:drive` | Asset browsers, content pickers |
| `teachfloor.dashboard.automation.list` | Automations | `/:org/automations` | Workflow inspectors, audit tools |
| `teachfloor.dashboard.getstarted.detail` | Get started | `/:org/getstarted` | Onboarding hints, checklists |
| **App Settings** |
| `settings` | App settings | `/:org/settings/apps/:appId` | App configuration, user preferences |
### Course Management
#### `teachfloor.dashboard.course.list`
**Displays on**: Course listing page
**Path**: `/:organization/courses`
**Use cases**: Course directory widgets, filters, quick actions
```json
{
"viewport": "teachfloor.dashboard.course.list",
"component": "CourseListView"
}
```
#### `teachfloor.dashboard.course.detail`
**Displays on**: Individual course page
**Path**: `/:organization/courses/:courseId`
**Use cases**: Course analytics, notes, supplementary materials
```json
{
"viewport": "teachfloor.dashboard.course.detail",
"component": "CourseDetailView"
}
```
#### `teachfloor.dashboard.course.module.detail`
**Displays on**: Module detail page
**Path**: `/:organization/courses/:courseId/modules/:moduleId`
**Use cases**: Module-specific tools, notes, progress tracking
```json
{
"viewport": "teachfloor.dashboard.course.module.detail",
"component": "ModuleDetailView"
}
```
#### `teachfloor.dashboard.course.element.detail`
**Displays on**: Learning element page (assignments, videos, etc.)
**Path**: `/:organization/courses/:courseId/modules/:moduleId/elements/:elementId`
**Use cases**: Element annotations, supplementary content, tools
```json
{
"viewport": "teachfloor.dashboard.course.element.detail",
"component": "ElementDetailView"
}
```
#### `teachfloor.dashboard.course.module.list`
**Displays on**: Module list page
**Path**: `/:organization/courses/:courseId/modules`
**Use cases**: Module overview, navigation tools
```json
{
"viewport": "teachfloor.dashboard.course.module.list",
"component": "ModuleListView"
}
```
#### `teachfloor.dashboard.course.assessment.list`
**Displays on**: Assessment list page
**Path**: `/:organization/courses/:courseId/assessments`
**Use cases**: Assessment overview, quiz management
```json
{
"viewport": "teachfloor.dashboard.course.assessment.list",
"component": "AssessmentListView"
}
```
#### `teachfloor.dashboard.course.progress.detail`
**Displays on**: Course progress page
**Path**: `/:organization/courses/:courseId/progress`
**Use cases**: Progress tracking, completion status
```json
{
"viewport": "teachfloor.dashboard.course.progress.detail",
"component": "CourseProgressView"
}
```
#### `teachfloor.dashboard.course.calendar.detail`
**Displays on**: Course calendar page
**Path**: `/:organization/courses/:courseId/calendar`
**Use cases**: Schedule view, deadline tracking
```json
{
"viewport": "teachfloor.dashboard.course.calendar.detail",
"component": "CourseCalendarView"
}
```
#### `teachfloor.dashboard.course.member.list`
**Displays on**: Course members page
**Path**: `/:organization/courses/:courseId/directory`
**Use cases**: Learner list, member management
```json
{
"viewport": "teachfloor.dashboard.course.member.list",
"component": "CourseMemberListView"
}
```
### Community
#### `teachfloor.dashboard.community.overview`
**Displays on**: Community home (cross-channel feed) — the `/community` landing page
**Path**: `/:organization/community`
**Use cases**: Cross-channel feed widgets, suggested content, digest tools
The workspace-level home that aggregates posts across every channel the viewer can see. Stripe-style `overview` rather than `feed`/`list` because no single resource is being listed — distinct from `community.channel.detail`, which targets a single channel.
```json
{
"viewport": "teachfloor.dashboard.community.overview",
"component": "CommunityOverviewView"
}
```
#### `teachfloor.dashboard.community.channel.detail`
**Displays on**: Single channel page
**Path**: `/:organization/community/:channelId`
**Use cases**: Social features, engagement tools, moderation
The primary resource on this page is the Channel itself — posts are its content — so the viewport follows Stripe's `{resource}.detail` convention.
```json
{
"viewport": "teachfloor.dashboard.community.channel.detail",
"component": "CommunityChannelView"
}
```
#### `teachfloor.dashboard.community.post.detail`
**Displays on**: Individual community post page
**Path**: `/:organization/community/:channelId/posts/:postId`
**Use cases**: Post sidebars, reaction tools, translation, attached references
```json
{
"viewport": "teachfloor.dashboard.community.post.detail",
"component": "CommunityPostView"
}
```
#### `teachfloor.dashboard.community.member.list`
**Displays on**: Community members directory
**Path**: `/:organization/community/:channelId/members`
**Use cases**: Member profiles, networking tools, search
```json
{
"viewport": "teachfloor.dashboard.community.member.list",
"component": "CommunityMembersView"
}
```
#### `teachfloor.dashboard.community.event.list`
**Displays on**: Community events list
**Path**: `/:organization/community/events`
**Use cases**: Calendar exports, RSVP tooling, scheduling helpers
```json
{
"viewport": "teachfloor.dashboard.community.event.list",
"component": "CommunityEventsView"
}
```
#### `teachfloor.dashboard.community.event.detail`
**Displays on**: Single community event page
**Path**: `/:organization/community/events/:eventId`
**Use cases**: Attendee tools, meeting integrations, reminders, follow-up content
```json
{
"viewport": "teachfloor.dashboard.community.event.detail",
"component": "CommunityEventView"
}
```
### Settings
#### `teachfloor.dashboard.settings.general.detail`
**Displays on**: General settings page
**Path**: `/:organization/settings/general`
**Use cases**: Organization-wide integrations, preferences
```json
{
"viewport": "teachfloor.dashboard.settings.general.detail",
"component": "GeneralSettingsView"
}
```
#### `teachfloor.dashboard.settings.customization.domain.detail`
**Displays on**: Customization · Domain subtab
**Path**: `/:organization/settings/customization/domain`
**Use cases**: Domain configuration widgets, DNS helpers
```json
{
"viewport": "teachfloor.dashboard.settings.customization.domain.detail",
"component": "CustomizationDomainView"
}
```
#### `teachfloor.dashboard.settings.customization.appearance.detail`
**Displays on**: Customization · Appearance subtab
**Path**: `/:organization/settings/customization/appearance`
**Use cases**: Theme tools, brand previews
```json
{
"viewport": "teachfloor.dashboard.settings.customization.appearance.detail",
"component": "CustomizationAppearanceView"
}
```
#### `teachfloor.dashboard.settings.customization.smtp.detail`
**Displays on**: Customization · SMTP subtab
**Path**: `/:organization/settings/customization/smtp`
**Use cases**: Email-delivery diagnostics
```json
{
"viewport": "teachfloor.dashboard.settings.customization.smtp.detail",
"component": "CustomizationSmtpView"
}
```
#### `teachfloor.dashboard.settings.customization.checkout.detail`
**Displays on**: Customization · Checkout subtab
**Path**: `/:organization/settings/customization/checkout`
**Use cases**: Checkout experiments, payment tweaks
```json
{
"viewport": "teachfloor.dashboard.settings.customization.checkout.detail",
"component": "CustomizationCheckoutView"
}
```
#### `teachfloor.dashboard.settings.customization.login.detail`
**Displays on**: Customization · Login subtab
**Path**: `/:organization/settings/customization/login`
**Use cases**: Login-screen branding
```json
{
"viewport": "teachfloor.dashboard.settings.customization.login.detail",
"component": "CustomizationLoginView"
}
```
#### `teachfloor.dashboard.settings.customization.labels.detail`
**Displays on**: Customization · Labels subtab
**Path**: `/:organization/settings/customization/labels`
**Use cases**: Custom terminology tools
```json
{
"viewport": "teachfloor.dashboard.settings.customization.labels.detail",
"component": "CustomizationLabelsView"
}
```
#### `teachfloor.dashboard.settings.customization.achievements.detail`
**Displays on**: Customization · Achievements subtab (visible only when the workspace has the achievements feature enabled)
**Path**: `/:organization/settings/customization/achievements`
**Use cases**: Gamification configuration
```json
{
"viewport": "teachfloor.dashboard.settings.customization.achievements.detail",
"component": "CustomizationAchievementsView"
}
```
#### `teachfloor.dashboard.settings.customization.profile-layout.detail`
**Displays on**: Customization · Profile layout subtab (visible only to owners and admins)
**Path**: `/:organization/settings/customization/profile-layout`
**Use cases**: Profile field arrangement tools
```json
{
"viewport": "teachfloor.dashboard.settings.customization.profile-layout.detail",
"component": "CustomizationProfileLayoutView"
}
```
#### `teachfloor.dashboard.settings.customization.dashboards.detail`
**Displays on**: Customization · Dashboards subtab — the admin page for managing home-dashboard layouts and widgets
**Path**: `/:organization/settings/customization/dashboards`
**Use cases**: Widget catalogs, layout authoring tools, dashboard templates
```json
{
"viewport": "teachfloor.dashboard.settings.customization.dashboards.detail",
"component": "CustomizationDashboardsView"
}
```
#### `teachfloor.dashboard.settings.team.list`
**Displays on**: Team management page
**Path**: `/:organization/settings/team`
**Use cases**: Team collaboration tools, role management
```json
{
"viewport": "teachfloor.dashboard.settings.team.list",
"component": "TeamSettingsView"
}
```
#### `teachfloor.dashboard.settings.billing.detail`
**Displays on**: Billing settings page
**Path**: `/:organization/settings/billing`
**Use cases**: Usage analytics, cost tracking
```json
{
"viewport": "teachfloor.dashboard.settings.billing.detail",
"component": "BillingView"
}
```
#### `teachfloor.dashboard.settings.integration.list`
**Displays on**: Integrations page
**Path**: `/:organization/settings/integrations`
**Use cases**: Third-party integrations, API tools
```json
{
"viewport": "teachfloor.dashboard.settings.integration.list",
"component": "IntegrationView"
}
```
#### `teachfloor.dashboard.settings.notification.list`
**Displays on**: Notification settings page
**Path**: `/:organization/settings/notifications`
**Use cases**: Custom notification rules, digest tools
```json
{
"viewport": "teachfloor.dashboard.settings.notification.list",
"component": "NotificationView"
}
```
#### `teachfloor.dashboard.settings.custom-field.list`
**Displays on**: Custom fields settings page
**Path**: `/:organization/settings/custom-fields`
**Use cases**: Custom field management, data import tools
```json
{
"viewport": "teachfloor.dashboard.settings.custom-field.list",
"component": "CustomFieldView"
}
```
#### `teachfloor.dashboard.settings.branch.list`
**Displays on**: Branches settings page
**Path**: `/:organization/settings/branches`
**Use cases**: Branch management, multi-tenant tools
```json
{
"viewport": "teachfloor.dashboard.settings.branch.list",
"component": "BranchListView"
}
```
#### `teachfloor.dashboard.settings.sanction.list`
**Displays on**: Sanctions settings page (visible only when the workspace has the sanctions feature enabled)
**Path**: `/:organization/settings/sanctions`
**Use cases**: Restriction policies, moderation tooling
```json
{
"viewport": "teachfloor.dashboard.settings.sanction.list",
"component": "SanctionListView"
}
```
#### `teachfloor.dashboard.settings.import.list`
**Displays on**: Import data page
**Path**: `/:organization/settings/import`
**Use cases**: Bulk import tools, data migration
```json
{
"viewport": "teachfloor.dashboard.settings.import.list",
"component": "ImportView"
}
```
#### `teachfloor.dashboard.settings.app.list`
**Displays on**: Apps settings page
**Path**: `/:organization/settings/apps`
**Use cases**: Installed apps, app marketplace
```json
{
"viewport": "teachfloor.dashboard.settings.app.list",
"component": "AppListView"
}
```
### User Management
#### `teachfloor.dashboard.account.detail`
**Displays on**: User account settings page
**Path**: `/:organization/account`
**Use cases**: Personal tools, user preferences, integrations
```json
{
"viewport": "teachfloor.dashboard.account.detail",
"component": "AccountView"
}
```
#### `teachfloor.dashboard.profile.detail`
**Displays on**: User profile page
**Path**: `/:organization/users/:userId`
**Use cases**: Profile widgets, member insights, supplementary user info
```json
{
"viewport": "teachfloor.dashboard.profile.detail",
"component": "ProfileDetailView"
}
```
#### `teachfloor.dashboard.learner.list`
**Displays on**: Learners management page
**Path**: `/:organization/learners`
**Use cases**: Learner analytics, bulk actions, import tools
```json
{
"viewport": "teachfloor.dashboard.learner.list",
"component": "LearnerListView"
}
```
### Commerce
#### `teachfloor.dashboard.payment.list`
**Displays on**: Payments page
**Path**: `/:organization/payments`
**Use cases**: Payment analytics, invoicing tools
```json
{
"viewport": "teachfloor.dashboard.payment.list",
"component": "PaymentView"
}
```
### Messaging
#### `teachfloor.dashboard.messaging.thread.detail`
**Displays on**: Messaging thread page
**Path**: `/:organization/messaging/threads/:threadId`
**Use cases**: Thread sidebars, translation tools, message-level integrations
```json
{
"viewport": "teachfloor.dashboard.messaging.thread.detail",
"component": "MessagingThreadView"
}
```
### Workspace
#### `teachfloor.dashboard.dashboard.detail`
**Displays on**: Home dashboard page — the user's landing screen with widget-based insights
**Path**: `/:organization/dashboard`
**Use cases**: KPI widgets, welcome banners, announcements, custom org-level insights
```json
{
"viewport": "teachfloor.dashboard.dashboard.detail",
"component": "DashboardView"
}
```
#### `teachfloor.dashboard.analytics.detail`
**Displays on**: Analytics page
**Path**: `/:organization/analytics`
**Use cases**: Custom dashboards, exports, alerts
```json
{
"viewport": "teachfloor.dashboard.analytics.detail",
"component": "AnalyticsView"
}
```
#### `teachfloor.dashboard.library.detail`
**Displays on**: Library drive page
**Path**: `/:organization/library/:drive`
**Use cases**: Asset browsers, content pickers
```json
{
"viewport": "teachfloor.dashboard.library.detail",
"component": "LibraryDriveView"
}
```
#### `teachfloor.dashboard.automation.list`
**Displays on**: Automations page
**Path**: `/:organization/automations`
**Use cases**: Workflow inspectors, audit tools
```json
{
"viewport": "teachfloor.dashboard.automation.list",
"component": "AutomationListView"
}
```
#### `teachfloor.dashboard.getstarted.detail`
**Displays on**: Get started page
**Path**: `/:organization/getstarted`
**Use cases**: Onboarding hints, checklists
```json
{
"viewport": "teachfloor.dashboard.getstarted.detail",
"component": "GetStartedView"
}
```
### App Settings
#### `settings`
**Displays on**: Your app's settings page
**Path**: `/:organization/settings/apps/:appId`
**Use cases**: App configuration, user preferences
```json
{
"viewport": "settings",
"component": "AppSettingsView"
}
```
:::info
The `settings` viewport provides a dedicated settings page for your app.
:::
## Viewport Context
When your app renders in a viewport, it receives context about the current page:
```javascript
import { useExtensionContext } from '@teachfloor/extension-kit'
const MyView = () => {
const { environment } = useExtensionContext()
console.log(environment.viewport) // "teachfloor.dashboard.course.detail"
console.log(environment.path) // "/myorg/courses/123"
return
Current viewport: {environment.viewport}
}
```
### Available Context
```typescript
environment: {
initialized: boolean // SDK ready state
viewport: string // Current viewport ID
path: string // Current URL path
}
```
## Viewport Matching
### How Matching Works
1. User navigates to a page (e.g., `/myorg/courses`)
2. Platform determines viewport (`teachfloor.dashboard.course.list`)
3. Platform finds apps with views matching the exact viewport
4. Matching app views are rendered in their designated positions
:::caution
Viewports require exact string matches. Wildcard patterns are not supported.
:::
### Example Scenario
Your app manifest:
```json
{
"ui_extension": {
"views": [
{
"viewport": "teachfloor.dashboard.course.detail",
"component": "CourseDetailView"
},
{
"viewport": "teachfloor.dashboard.course.list",
"component": "CourseListView"
},
{
"viewport": "settings",
"component": "SettingsView"
}
]
}
}
```
**User visits course detail page (`/org/courses/123`)**:
- ✅ `CourseDetailView` renders (viewport: `teachfloor.dashboard.course.detail`)
- ❌ `CourseListView` does not render (different viewport)
**User visits course list page (`/org/courses`)**:
- ✅ `CourseListView` renders (viewport: `teachfloor.dashboard.course.list`)
- ❌ `CourseDetailView` does not render (different viewport)
**User visits app settings page (`/org/settings/apps/yourapp`)**:
- ✅ `SettingsView` renders (viewport: `settings`)
## View Component Structure
### Basic View Component
```jsx
import React from 'react'
import { Container, Text, useExtensionContext } from '@teachfloor/extension-kit'
const CourseListView = () => {
const { environment, userContext } = useExtensionContext()
return (
Hello {userContext.full_name}
You're viewing: {environment.viewport}
)
}
export default CourseListView
```
### Viewport-Aware Component
```jsx
import React from 'react'
import { useExtensionContext } from '@teachfloor/extension-kit'
const SmartView = () => {
const { environment } = useExtensionContext()
// Render different UI based on viewport
if (environment.viewport.includes('course.detail')) {
return
}
if (environment.viewport.includes('settings')) {
return
}
return
}
```
### Dynamic View Loader
```jsx
import React from 'react'
import { ExtensionViewLoader } from '@teachfloor/extension-kit'
import manifest from '../../teachfloor-app.json'
const App = () => {
return (
import(`./${componentName}`)}
/>
)
}
export default App
```
## Adding Views to Your App
### Using CLI
```bash
teachfloor apps add view
```
1. Select viewport from list
2. Enter component name (PascalCase)
3. Choose whether to generate example code
### Manual Addition
1. Create component file:
```bash
touch src/views/CourseListView.jsx
```
2. Update manifest:
```json
{
"ui_extension": {
"views": [
{
"viewport": "teachfloor.dashboard.course.list",
"component": "CourseListView"
}
]
}
}
```
3. Implement component:
```jsx
import React from 'react'
import { Container, Text } from '@teachfloor/extension-kit'
const CourseListView = () => {
return (
My Course List Widget
)
}
export default CourseListView
```
## Removing Views
### Using CLI
```bash
teachfloor apps remove view
```
### Manual Removal
1. Delete component file
2. Remove from manifest:
```json
{
"ui_extension": {
"views": [
// Remove the view object
]
}
}
```
## Next Steps
→ Continue to [Extension Kit Components](./extension-kit/components)
## Additional Resources
- [Best Practices](/docs/apps/references/best-practices) - Viewport selection and performance optimization
- [Troubleshooting Guide](/docs/apps/references/troubleshooting) - Viewport issues and debugging
- [Examples](/docs/apps/references/examples) - Multi-viewport app examples
- [Extension Kit Integration](./extension-kit/integration)
---
## Document: /docs/apps/core-concepts/surfaces
URL: /docs/apps/core-concepts/surfaces
# Surfaces
Surfaces describe **how** your app renders inside Teachfloor — as a drawer that slides in from the app dock, or as a widget embedded inline in a layout. Combined with a [viewport](./viewports), the pair `(viewport, surface)` fully specifies where and how your app is mounted.
## Two Axes: Viewport and Surface
Every entry in your manifest's `ui_extension.views` is defined by two orthogonal properties:
| Axis | Answers | Example |
|---|---|---|
| **Viewport** | *Where* is the user? | `teachfloor.dashboard.course.detail` |
| **Surface** | *How* is your app rendered? | `drawer` or `widget` |
An app can register **multiple entries at the same viewport** as long as they use different surfaces (or, for widgets, different widget ids — see [Multiple Widgets](#multiple-widgets-at-the-same-viewport)). Kit picks the correct component using both fields.
## Available Surfaces
### Drawer
A right-side panel opened from the app dock. This is the default surface — if a manifest entry omits `surface`, it renders as a drawer for back-compat with pre-surfaces apps.
**Typical use:** on-demand focus interactions — open the app, look at something, close it. Cohort presence, quick notes, calculators, help articles.
```json
{
"surface": "drawer",
"viewport": "teachfloor.dashboard.course.detail",
"component": "CourseHelper"
}
```
The `surface` field is optional here — omitting it produces the same result:
```json
{
"viewport": "teachfloor.dashboard.course.detail",
"component": "CourseHelper"
}
```
### Widget
A cell in a layout that hosts widget slots. Widgets are always visible on the page (no click-to-open) and sit alongside native platform widgets.
**Typical use:** always-on displays — dashboards, progress panels, live counters, feature highlights.
Widget entries carry an extra `widget` block with three required fields:
```json
{
"surface": "widget",
"viewport": "teachfloor.dashboard.dashboard.detail",
"component": "CourseProgressWidget",
"widget": {
"id": "course_progress",
"name": "Course Progress",
"description": "Aggregate progress across all enrolled courses."
}
}
```
- **`id`** — stable slug (`^[a-z][a-z0-9_]*$`), **unique per app** across all widget declarations. Used as the runtime identifier when your bundle contains multiple widget components. Part of the composite key stored in the dashboard row, so renaming your component doesn't invalidate placed layouts.
- **`name`** — shown in the admin's widget picker and in the app install-consent surface list. ≤60 chars.
- **`description`** — one-line explanation shown alongside the name in the picker. ≤200 chars.
## A Full Manifest Example
An app registering a drawer plus two widgets:
```json
{
"id": "6a42a271ae2ee",
"version": "1.0.0",
"name": "Course Progress Panel",
"description": "See your progress across all enrolled courses.",
"distribution_type": "public",
"ui_extension": {
"views": [
{
"surface": "drawer",
"viewport": "teachfloor.dashboard.dashboard.detail",
"component": "ProgressDetailView"
},
{
"surface": "widget",
"viewport": "teachfloor.dashboard.dashboard.detail",
"component": "CourseProgressWidget",
"widget": {
"id": "course_progress",
"name": "Course Progress",
"description": "Aggregate progress across all enrolled courses."
}
},
{
"surface": "widget",
"viewport": "teachfloor.dashboard.dashboard.detail",
"component": "LearningStreakWidget",
"widget": {
"id": "learning_streak",
"name": "Learning Streak",
"description": "Current daily study streak with a 7-day heatmap."
}
}
]
},
"permissions": []
}
```
The two widgets share the same viewport — Kit disambiguates by `widget.id` at render time.
## Reading the Surface at Runtime
The current surface is exposed via `useExtensionContext()`, grouped inside `environment`:
```jsx
import { useExtensionContext, SURFACES } from '@teachfloor/extension-kit'
const MyView = () => {
const { environment } = useExtensionContext()
if (environment.surface === SURFACES.WIDGET) {
// compact layout — widget slot is usually small
}
if (environment.surface === SURFACES.DRAWER) {
// full drawer layout with headers and sections
}
}
```
Prefer the `SURFACES` constant over the bare `'drawer'` / `'widget'` strings — it's exported specifically to survive future renames.
Apps that don't need surface-aware behavior can ignore `environment.surface` entirely — most components render the same regardless of surface.
## Widget Auto-Height with ``
**Requires `@teachfloor/extension-kit` ≥ 1.27.0.** Earlier versions of the kit don't ship `` or the `SURFACES` constant. Bump your app's dependency before wrapping widgets in ``.
Wrap your widget's root element in `` and the slot in the dashboard grid will automatically fit your content's height. No aspect ratio needed:
```jsx
import { WidgetView, Text, Stack } from '@teachfloor/extension-kit'
const LearningStreakWidget = () => (
7-day streakKeep going — you're on fire.
)
```
`WidgetView` accepts the same layout props as `Container` (`p`, `px`, `py`, `sx`, etc.) — spacing shorthands work directly.
Under the hood, `WidgetView` observes its own DOM height and emits it to the host over RPC. The host applies the reported height to the slot's container, so a widget with sparse content stays compact and a widget with a scrolling list grows to match. The same mechanism powers ``.
**Admin override:** if the dashboard admin explicitly picks an aspect ratio when configuring the widget slot, that ratio wins over the emitted height. Auto-height is the default; forced ratios are the escape hatch.
## Multiple Widgets at the Same Viewport
A single app bundle can register multiple widgets at the same viewport as long as each has a distinct `widget.id`. This lets one deploy provide, say, a *Course Progress* widget and a *Learning Streak* widget from the same codebase.
Kit picks each widget's component using `environment.view.id`:
```jsx
import { useExtensionContext, SURFACES } from '@teachfloor/extension-kit'
import CourseProgressWidget from './widgets/CourseProgressWidget'
import LearningStreakWidget from './widgets/LearningStreakWidget'
const WIDGETS = {
course_progress: CourseProgressWidget,
learning_streak: LearningStreakWidget,
}
const App = () => {
const { environment } = useExtensionContext()
if (environment.surface === SURFACES.WIDGET) {
const Component = WIDGETS[environment.view.id]
return Component ? : null
}
return
}
```
In most cases you can skip this dispatch entirely and let `ExtensionViewLoader` (from `@teachfloor/extension-kit`) resolve the right component by mapping `widget.id` to your components — see the [Extension Kit Components](./extension-kit/components) chapter.
### Runtime `environment.viewport` is always the actual route
A widget declared as `viewport: '*'` that lands on the dashboard receives `environment.viewport === 'teachfloor.dashboard.dashboard.detail'` at runtime — never `'*'`. Wildcards are declaration scope; the runtime value is always concrete. This lets your widget code react to *where it landed* (fetch course-specific data on a course page, org-wide data on the main dashboard) without conflating declaration with location.
## CLI Helpers
The Teachfloor CLI scaffolds drawer views and widgets with the correct manifest shape:
```bash
# Add a new drawer view (prompts for viewport + component name)
teachfloor apps add view
# Add a new widget (prompts for viewport, widget id, name, description, component)
teachfloor apps add widget
# Remove a widget by id
teachfloor apps remove widget
```
See the [CLI reference](./references/cli) for the full flag list.
## Continue to
- [Extension Kit Components](./extension-kit/components) — the UI primitives you'll use inside your surfaces, including `` and ``
- [Extension Kit Integration](./extension-kit/integration) — surface-agnostic APIs (events, storage, navigation) that work identically across drawer and widget
---
## Document: /docs/apps/core-concepts/extension-kit/components
URL: /docs/apps/core-concepts/extension-kit/components
# Extension Kit Components
This guide covers the **visual components** provided by the Teachfloor Extension Kit (`@teachfloor/extension-kit`) - everything you need to build your app's user interface.
## What This Document Covers
This document focuses exclusively on **UI components** for building your app's interface:
- **Form Components**: TextInput, Select, Checkbox, Switch, etc.
- **Layout Components**: Container, Grid, Stack, Group
- **Display Components**: Button, Text, Badge, Avatar, Tabs
- **Chart Components**: BarChart, LineChart
- **Special Components**: SettingsView
- **Setup Components**: ExtensionContextProvider, ExtensionViewLoader
All components are designed to match Teachfloor's design system, ensuring your app feels native to the platform.
### Quick Reference
| What you need | Where to find it |
|---------------|------------------|
| Buttons, Forms, Layouts, Charts | **This document** (05-components.md) |
| Events, Storage, Navigation, Toasts | [Extension Kit Integration](./integration) |
:::info
**Looking for platform integration?** To communicate with the platform (events, storage, navigation, toasts), see the [Extension Kit Integration](./integration) guide.
:::
## Installation
The Extension Kit is automatically included when you create an app with the CLI:
```bash
teachfloor apps create my-app
```
To install manually:
```bash
npm install @teachfloor/extension-kit react react-dom
```
## Setup Components
### Extension Context Provider
Wrap your app with the provider to enable platform context throughout your app:
```jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { ExtensionContextProvider } from '@teachfloor/extension-kit'
import App from './App'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
)
```
**Props**:
- `autoInit`: (Optional) Automatically signal app ready to platform (default: true)
:::info
**Accessing context data?** To use the `useExtensionContext()` hook and access platform data, see the [Extension Kit Integration](./integration#context-api) guide.
:::
### View Loader
Automatically load the correct component based on viewport:
```jsx
import React from 'react'
import { ExtensionViewLoader } from '@teachfloor/extension-kit'
import manifest from '../teachfloor-app.json'
const App = () => {
return (
import(`./views/${componentName}`)}
/>
)
}
export default App
```
**Props**:
- `manifest`: Your app's manifest object (required)
- `componentResolver`: Function that returns a dynamic import (optional, defaults to null)
- `fallback`: Loading component (optional, defaults to null)
- `basePath`: Base path for components (optional, defaults to `'./'`)
## Layout Components
### Container
Main content wrapper with consistent padding:
```jsx
import { Container } from '@teachfloor/extension-kit'
```
**Props**:
- All standard HTML div props
- Spacing props: `p`, `m`, `px`, `py`, `mx`, `my`
### Grid
Responsive grid layout:
```jsx
import { Grid } from '@teachfloor/extension-kit'
Half width
Half width
// Responsive columns
Responsive column
```
**Props**:
- `gutter`: Gap between columns
- `align`: Vertical alignment
- `justify`: Horizontal alignment
### SimpleGrid
Automatic grid with equal columns:
```jsx
import { SimpleGrid } from '@teachfloor/extension-kit'
Item 1
Item 2
Item 3
// Responsive
Item 1
Item 2
Item 3
```
**Props**:
- `cols`: Number of columns
- `spacing`: Gap between items
- `verticalSpacing`: Vertical gap
### Group
Horizontal layout with flex:
```jsx
import { Group } from '@teachfloor/extension-kit'
```
**Props**:
- `spacing`: Gap between items
- `position`: `"left"` | `"center"` | `"right"` | `"apart"`
- `align`: Vertical alignment
## Form Components
### TextInput
Single-line text input:
```jsx
import { TextInput } from '@teachfloor/extension-kit'
console.log(e.target.value)}
/>
// With validation
```
**Props**:
- `label`: Input label
- `placeholder`: Placeholder text
- `required`: Required field
- `error`: Error message
- `description`: Help text
- All standard input props
### Textarea
Multi-line text input:
```jsx
import { Textarea } from '@teachfloor/extension-kit'
```
**Props**:
- `minRows`: Minimum rows
- `maxRows`: Maximum rows
- `autosize`: Auto-grow with content
- All TextInput props
### Select
Dropdown selection:
```jsx
import { Select } from '@teachfloor/extension-kit'
```
**Props**:
- `data`: Array of options
- `searchable`: Enable search
- `clearable`: Allow clearing
- All TextInput props
### MultiSelect
Multiple selection dropdown:
```jsx
import { MultiSelect } from '@teachfloor/extension-kit'
```
### NumberInput
Numeric input with controls:
```jsx
import { NumberInput } from '@teachfloor/extension-kit'
```
### PasswordInput
Password input with toggle:
```jsx
import { PasswordInput } from '@teachfloor/extension-kit'
```
### Checkbox
Checkbox input:
```jsx
import { Checkbox } from '@teachfloor/extension-kit'
setChecked(e.target.checked)}
/>
```
### Radio
Radio button input:
```jsx
import { Radio } from '@teachfloor/extension-kit'
```
### Switch
Toggle switch:
```jsx
import { Switch } from '@teachfloor/extension-kit'
setEnabled(e.target.checked)}
/>
```
### ColorInput
Color picker:
```jsx
import { ColorInput } from '@teachfloor/extension-kit'
```
## Display Components
### Text
Styled text component:
```jsx
import { Text } from '@teachfloor/extension-kit'
Large bold textSmall dimmed textError message
```
**Props**:
- `size`: `"xs"` | `"sm"` | `"md"` | `"lg"` | `"xl"`
- `fw`: Font weight (100-900)
- `c`: Color
- `align`: Text alignment
### Button
Action button:
```jsx
import { Button } from '@teachfloor/extension-kit'
// Variants
// Sizes
// Colors
// Loading state
// Disabled
```
**Props**:
- `variant`: `"filled"` | `"outline"` | `"subtle"`
- `size`: `"xs"` | `"sm"` | `"md"` | `"lg"`
- `color`: Color name
- `loading`: Show loading state
- `disabled`: Disable button
### ButtonGroup
Group related buttons:
```jsx
import { ButtonGroup } from '@teachfloor/extension-kit'
```
### Badge
Label or status indicator:
```jsx
import { Badge } from '@teachfloor/extension-kit'
DefaultInfoSuccessErrorWith dot
```
### Chip
Interactive chip/tag:
```jsx
import { Chip } from '@teachfloor/extension-kit'
Selectable Chip
```
### Avatar
User avatar:
```jsx
import { Avatar } from '@teachfloor/extension-kit'
ABJD
```
**Props**:
- `src`: Image URL
- `alt`: Alt text
- `radius`: Border radius
- `size`: Size
- `color`: Background color for initials
### Image
Optimized image component:
```jsx
import { Image } from '@teachfloor/extension-kit'
```
### Divider
Visual separator:
```jsx
import { Divider } from '@teachfloor/extension-kit'
```
### Tooltip
Hover tooltip:
```jsx
import { Tooltip } from '@teachfloor/extension-kit'
```
### Loader
Loading indicator:
```jsx
import { Loader } from '@teachfloor/extension-kit'
```
### Tabs
Tab navigation:
```jsx
import { Tabs } from '@teachfloor/extension-kit'
First TabSecond Tab
First panel
Second panel
```
## Chart Components
### BarChart
Bar chart visualization:
```jsx
import { BarChart } from '@teachfloor/extension-kit'
const data = [
{ name: 'Jan', value: 400 },
{ name: 'Feb', value: 300 },
{ name: 'Mar', value: 500 }
]
```
### LineChart
Line chart visualization:
```jsx
import { LineChart } from '@teachfloor/extension-kit'
const data = [
{ name: 'Week 1', value: 100 },
{ name: 'Week 2', value: 150 },
{ name: 'Week 3', value: 120 }
]
```
## Special Components
### SettingsView
Pre-built settings page template:
```jsx
import { SettingsView, TextInput, Select, SimpleGrid } from '@teachfloor/extension-kit'
import { useState } from 'react'
const AppSettings = () => {
const [status, setStatus] = useState('')
const saveSettings = async (values) => {
setStatus('Saving...')
try {
await fetch('https://api.example.com/save', {
method: 'POST',
body: JSON.stringify(values)
})
setStatus('Saved!')
} catch (error) {
setStatus('Error saving settings')
}
}
return (
)
}
```
**Props**:
- `onSave`: Save handler function
- `statusMessage`: Status message to display
- `children`: Form fields
## Spacing System
All components support spacing props:
| Prop | Description |
|------|-------------|
| `p` | Padding (all sides) |
| `px` | Padding horizontal |
| `py` | Padding vertical |
| `pt` | Padding top |
| `pr` | Padding right |
| `pb` | Padding bottom |
| `pl` | Padding left |
| `m` | Margin (all sides) |
| `mx` | Margin horizontal |
| `my` | Margin vertical |
| `mt` | Margin top |
| `mr` | Margin right |
| `mb` | Margin bottom |
| `ml` | Margin left |
**Values**: `xs`, `sm`, `md`, `lg`, `xl` or pixel number
**Example**:
```jsx
ContentContent
```
## Color System
Available colors:
- `blue`, `red`, `green`, `yellow`, `orange`
- `purple`, `pink`, `teal`, `cyan`
- `gray`, `dark`, `dimmed`
**Example**:
```jsx
Blue textSuccess
```
## Next Steps
Learn how to integrate with the platform:
→ Continue to [Extension Kit Integration](./integration)
## Additional Resources
- [Extension Kit Integration](./integration) - Events, storage, navigation, and platform integration
- [Data Storage](/docs/apps/advanced-topics/data-storage) - Deep dive into data persistence
- [Examples](/docs/apps/references/examples) - Sample apps and code snippets
- [Best Practices](/docs/apps/references/best-practices) - Development patterns and tips
---
## Document: /docs/apps/core-concepts/extension-kit/integration
URL: /docs/apps/core-concepts/extension-kit/integration
# Extension Kit Integration
This guide covers platform integration functions provided by the Teachfloor Extension Kit (`@teachfloor/extension-kit`).
## What This Document Covers
This document focuses exclusively on **API functions** for platform integration:
### Core Functions
- `initialize()` - Signal that your app is ready
- `subscribeToEvent()` - Listen to platform events
- `store()`, `retrieve()`, `createStorage()`, `createCollection()` - Data storage and retrieval
- `showToast()` - Display notifications
- `showDrawer()`, `hideDrawer()`, `toggleDrawer()` - Control app drawer
- `openModal()`, `closeModal()` - Promote a widget into a modal container (widget surface only)
- `goToViewport()` - Navigate to different platform areas using viewports
- `goToPath()` - Navigate to different platform areas using paths
### React Hooks
- `useExtensionContext()` - Access user and platform data reactively
- `useLaunchState()` - Read the app-provided launch state passed by `openModal({ state })`
### Quick Reference
| What you need | Where to find it |
|---------------|------------------|
| Events, Storage, Navigation, Toasts | **This document** (06-integration.md) |
| Buttons, Forms, Layouts, Charts | [Extension Kit Components](./components) |
:::info
**Need UI components?** For buttons, inputs, layouts, and charts, see the [Extension Kit Components](./components) guide.
:::
:::info
All imports come from `@teachfloor/extension-kit`, which is automatically installed when you create an app.
:::
## Signaling App Ready
### Basic Usage
Signal to the platform that your app is ready:
```javascript
import { initialize } from '@teachfloor/extension-kit'
function App() {
useEffect(() => {
initialize() // Signal app is ready
}, [])
return
My App
}
```
### With Context Provider
```javascript
import { ExtensionContextProvider } from '@teachfloor/extension-kit'
// The provider signals readiness automatically
```
## API Reference
### Events
#### Subscribe to Events
Listen to platform events using `subscribeToEvent()`. Events receive two parameters: the event data and an `objectContext` containing contextual information based on your app's permissions.
```javascript
import { subscribeToEvent } from '@teachfloor/extension-kit'
// Viewport changes - objectContext includes course/module/element based on permissions
subscribeToEvent('environment.viewport.changed', (viewport, objectContext) => {
console.log('User navigated to:', viewport)
if (objectContext.course) {
console.log('Course:', objectContext.course.name)
}
if (objectContext.module) {
console.log('Module:', objectContext.module.name)
}
if (objectContext.element) {
console.log('Element:', objectContext.element.name)
}
})
// Path changes - single parameter
subscribeToEvent('environment.path.changed', (path) => {
console.log('Path changed:', path)
})
// User events - objectContext includes contextual data
subscribeToEvent('auth.user.event', (eventData, objectContext) => {
console.log('User event:', eventData.type)
if (objectContext.course) {
console.log('Happened in course:', objectContext.course.name)
}
})
```
**Available Events:**
| Event | Parameters | Description |
|-------|-----------|-------------|
| `environment.viewport.changed` | `(viewport, objectContext)` | User navigated to a different viewport. `objectContext` contains course/module/element data based on permissions. |
| `environment.path.changed` | `(path)` | URL path changed |
| `auth.user.event` | `(eventData, objectContext)` | User activity event (login, element_completed, quiz_submitted, etc.). `objectContext` contains contextual data. |
:::caution
The `objectContext` parameter only includes data for permissions declared in your app manifest.
:::
**objectContext Structure:**
```typescript
interface ObjectContext {
course?: {
id: string
object: 'course'
name: string
created_at: string
cover: string | null
availability: string
visibility: string
currency: string
price: number | null
metadata: object
}
module?: {
id: string
object: 'module'
name: string
created_at: string
cover: string | null
type: string
position: number
metadata: object
}
element?: {
id: string
object: 'element'
name: string
created_at: string
cover: string | null
type: string
position: number
metadata: object
}
}
```
:::caution
Objects are only present when the permission is granted and the viewport context matches.
:::
**How it works:**
Objects are included based on two conditions:
1. **Permission granted**: Your app must have the corresponding permission declared in the manifest
2. **Relevant context**: The current viewport/path must be within that context
When both conditions are met, the **complete object** is included. Otherwise, the key is **not present** in the object.
**Example:**
```javascript
// With courses:read permission on a course detail page:
{
course: { id: "abc123", name: "Introduction to React", /* ...other course fields */ }
// module and element keys not present (not in their context)
}
// Without courses:read permission on a course detail page:
{
// No keys present (no permissions granted)
}
// With courses:read and modules:read on a module detail page:
{
course: { id: "abc123", name: "Introduction to React", /* ...other course fields */ },
module: { id: "def456", name: "Getting Started", /* ...other module fields */ }
// element key not present (not in element context)
}
```
:::info
See [Permissions](/docs/apps/advanced-topics/permissions) for details on requesting access to course, module, and element data.
:::
### Data Storage
The Extension Kit provides `store()`, `retrieve()`, `createStorage()`, and `createCollection()` functions for persisting data. Three types of storage are available: app data (organization-wide), user data (user-specific), and user collections (paginated lists). `createStorage()` is the recommended wrapper over `store` / `retrieve`, adding namespaced keys, TTL, and `query()` for paged filter/sort iteration.
:::info
See [Data Storage](/docs/apps/advanced-topics/data-storage) for complete documentation, API reference, and usage examples.
:::
## Context API
### Accessing Context
```javascript
import { useExtensionContext } from '@teachfloor/extension-kit'
function MyComponent() {
const { userContext, appContext, environment } = useExtensionContext()
return (
User: {userContext.full_name}
App: {appContext.name}
Viewport: {environment.viewport}
)
}
```
### Context Structure
```typescript
interface Context {
userContext: {
id: string
created_at: string
full_name: string
email: string
avatar: string
language: string
timezone: string
identity_provider: { // SSO/Identity provider details (null if not configured)
provider: string // Provider name (e.g., 'auth0', 'saml', 'neoncrm')
user_id: string | null // User ID in the identity provider
user_metadata: object // Additional user metadata from provider
} | null
}
appContext: {
id: string
name: string
version: string // Current installed version
permissions: string[] // Array of granted permission scopes
views: array // Array of app view configurations from manifest
}
environment: {
initialized: boolean
viewport: string
path: string
surface: 'drawer' | 'page' | 'widget'
presentation: 'default' | 'modal' // 'modal' when the widget was opened via openModal()
}
// App-provided payload passed to openModal({ state }). Set once at
// mount and does not update.
// null when no launch state was provided.
state: object | null
}
```
### Reactive Context
Context updates automatically when user data changes:
```javascript
function UserGreeting() {
const { userContext } = useExtensionContext()
// Re-renders when userContext changes
return
Hello, {userContext.full_name}!
}
```
## UI Integration
### Toast Notifications
```javascript
import { showToast } from '@teachfloor/extension-kit'
// Success message
showToast('Changes saved successfully', { color: 'green' })
// Error message
showToast('Failed to save changes', { color: 'red' })
// Info message
showToast('Processing your request', { color: 'blue' })
// Warning message
showToast('Please review your input', { color: 'orange' })
```
**Options:**
```typescript
interface ToastOptions {
color?: 'green' | 'red' | 'blue' | 'orange'
autoClose?: number // milliseconds
}
```
### Drawer Control
```javascript
import { showDrawer, hideDrawer, toggleDrawer } from '@teachfloor/extension-kit'
// Show drawer
function handleOpen() {
showDrawer()
}
// Hide drawer
function handleClose() {
hideDrawer()
}
// Toggle drawer
function handleToggle() {
toggleDrawer()
}
// Auto-show on mount
useEffect(() => {
showDrawer()
return () => hideDrawer()
}, [])
```
### Modal Control
Widget-surface views can promote themselves into a modal for more room — useful for detail views, forms, or wizards triggered from a compact dashboard widget. The widget always opens ITSELF in the modal (never a different widget), and the modal runs the same component with a fresh mount.
```javascript
import { openModal, closeModal, useExtensionContext } from '@teachfloor/extension-kit'
function MyWidget() {
const { environment } = useExtensionContext()
const isModal = environment.presentation === 'modal'
// Compact rendering in the widget slot, full rendering in the modal
if (!isModal) {
return
}
return (
)
}
```
#### `openModal(options)`
Available on the **widget surface only**. Calls from drawer or page surfaces are silently ignored.
```typescript
interface ModalOptions {
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '100%' // default 'md'
closeOnClickOutside?: boolean // default true
closeOnEscape?: boolean // default true
state?: object // arbitrary launch state (see below)
}
```
Anything outside the allowed values is dropped by the host — you can't pass arbitrary strings through to the underlying modal.
#### `closeModal()`
Dismiss the modal from within its own view. Only meaningful when `environment.presentation === 'modal'`. Typical use: form submit success, wizard finish. Calls from a normally-placed widget/drawer/page are no-ops.
#### Launch state — `openModal({ state })` + `useLaunchState()`
Passing `state` in `openModal` hands an arbitrary object to the modal-hosted view. The child reads it via `useLaunchState()` (or `useExtensionContext().state`). Set once at mount, does not update. Use it for deep-linking, initial form values, or opener context.
```javascript
import { openModal, useLaunchState } from '@teachfloor/extension-kit'
// In the widget slot:
function CompactWidget() {
return (
)
}
// In the same widget rendered in the modal:
function ModalView() {
const launchState = useLaunchState() // { noteId: 'abc-123', mode: 'edit' } — or null
if (!launchState?.noteId) return
return
}
```
`useLaunchState()` returns `null` when the view wasn't launched with state (i.e. rendered directly in its widget slot, not opened via `openModal`).
### Navigation
```javascript
import { goToViewport } from '@teachfloor/extension-kit'
// Navigate to courses
function goToCourses() {
goToViewport('teachfloor.dashboard.course.list')
}
// Navigate to settings
function goToSettings() {
goToViewport('teachfloor.dashboard.settings.general.detail')
}
// Navigate to account
function goToAccount() {
goToViewport('teachfloor.dashboard.account.detail')
}
```
### Deeplinking
Use `goToPath` when you already have a fully-resolved in-app path — typically captured from `environment.path` — and want to deeplink the user back to it (e.g. a saved bookmark or a "back to where you were" button).
```javascript
import { goToPath, useExtensionContext } from '@teachfloor/extension-kit'
// Save the current path...
const { environment } = useExtensionContext()
const savedPath = environment.path // e.g. "/org-slug/courses/123/modules/456"
// ...and deeplink back later
goToPath(savedPath)
```
**Rules and guards:**
- `path` must be a relative path beginning with a single `/` (no protocol-relative `//…`, no absolute URLs). Anything else is ignored.
- The path must belong to the **current organization** — deeplinks whose first segment doesn't match the user's org slug are rejected, so an app installed in one org can't redirect the user into another.
- On custom domains, the org slug is stripped from the URL automatically — paths captured from `environment.path` work on both URL shapes.
## AI Generation
### Text Generation
Generate text using AI models with the platform's built-in AI capabilities.
```javascript
import { generate } from '@teachfloor/extension-kit'
// Generate text
async function generateContent() {
try {
const result = await generate(
'Write a summary of this course',
'ai/text-generate'
)
console.log(result) // Generated text response
return result
} catch (error) {
console.error('Generation failed:', error)
}
}
```
**Parameters:**
- `prompt` (string, required): The prompt to send to the AI model. Can include placeholders like `{{course.content}}`
- `generationType` (string, optional): Type of generation (default: `'ai/text-generate'`)
**Available Generation Types:**
- `'ai/text-generate'`: General text generation
**Permissions Required:**
- `ai:text_generate`: Always required to use AI generation
- `ai:context_external_send`: Only required when using placeholders
- Contextual permissions: Required for corresponding placeholders (`courses:read` for course placeholders, etc.)
### Using Placeholders
Include platform data directly in prompts using placeholders:
```javascript
import { generate } from '@teachfloor/extension-kit'
// Course placeholders (requires: ai:text_generate + courses:read + ai:context_external_send)
const courseSummary = await generate(
'Summarize this course: {{course.content}}'
)
// Module placeholders (requires: ai:text_generate + modules:read + ai:context_external_send)
const moduleQuiz = await generate(
'Create a 5-question quiz about {{module.name}}: {{module.content}}'
)
// Element placeholders (requires: ai:text_generate + elements:read + ai:context_external_send)
const studyNotes = await generate(
'Generate study notes for {{element.name}}: {{element.content}}'
)
```
**Supported Placeholders:**
- `{{course.name}}` - Course title
- `{{course.content}}` - Course content (text format)
- `{{module.name}}` - Module title
- `{{module.content}}` - Module content (text format)
- `{{element.name}}` - Element title
- `{{element.content}}` - Element content (text format)
### Without Placeholders
You can also manually include context data (only requires `ai:text_generate`):
```javascript
import { generate, useExtensionContext } from '@teachfloor/extension-kit'
function SummarizeButton() {
const { objectContext } = useExtensionContext()
const handleSummarize = async () => {
if (!objectContext.course) return
// Manually including context - no placeholders
const prompt = `Summarize this course:
Name: ${objectContext.course.name}
ID: ${objectContext.course.id}
`
const summary = await generate(prompt)
console.log(summary)
}
return
}
```
**Error Handling:**
```javascript
async function safeGenerate(prompt) {
try {
const result = await generate(prompt)
return { success: true, data: result }
} catch (error) {
if (error.message === 'Teachfloor is not available') {
return { success: false, error: 'Platform unavailable' }
}
return { success: false, error: error.message }
}
}
```
:::info
See [Permissions](/docs/apps/advanced-topics/permissions#ai-permissions) for more details on AI permissions.
:::
## Next Steps
→ Continue to [Data Storage](/docs/apps/advanced-topics/data-storage)
## Additional Resources
- [Extension Kit Components](./components) - UI components reference
- [Permissions](/docs/apps/advanced-topics/permissions) - Permission scopes and usage
- [Best Practices](/docs/apps/references/best-practices) - Integration patterns and error handling
- [Examples](/docs/apps/references/examples) - Integration examples
---
## Document: /docs/apps/advanced-topics/data-storage
URL: /docs/apps/advanced-topics/data-storage
# Data Storage
The Extension Kit provides data storage capabilities for persisting app data on the Teachfloor platform. Data is automatically scoped by organization and app, with optional user-level scoping.
## Overview
Three types of storage are available:
| Storage Type | Scope | Use Case |
|-------------|-------|----------|
| **App Data** | Organization + App | Shared settings, configurations |
| **User Data** | Organization + App + User | User-specific preferences, state |
| **User Collection** | Organization + App + User | Lists, activity logs, history |
Two API styles are available on top of App Data and User Data:
- **Raw primitives** (`store` / `retrieve`) — one row per key, described in the App Data / User Data sections below.
- **Storage Manager** (`createStorage`, **recommended**) — a namespaced wrapper adding TTL and `query()` for paged filter/sort iteration across many rows. See [Storage Manager](#storage-manager-recommended).
For per-key append semantics (many rows sharing a key, id-based CRUD), use [User Collection Storage](#user-collection-storage) instead.
:::caution
Each storage type requires appropriate read/write permissions. Write permissions automatically include read access. See [Permissions Reference](./permissions) for details.
:::
**Security**: All data is automatically encrypted at rest on the Teachfloor platform.
## App Data Storage
Store data shared across all users in your organization.
### Permissions Required
**Read and Write**:
```json
{
"permissions": [
{
"permission": "appdata:write",
"purpose": "Save and load app configuration and settings"
}
]
}
```
:::info
`appdata:write` automatically includes read access.
:::
**Read-Only** (if you only need to read):
```json
{
"permissions": [
{
"permission": "appdata:read",
"purpose": "Load app configuration and settings"
}
]
}
```
### Usage
```javascript
import { store, retrieve } from '@teachfloor/extension-kit'
// Store app-wide data
await store('settings', {
theme: 'dark',
language: 'en',
notifications: true
}, 'appdata')
// Retrieve app-wide data
const settings = await retrieve('settings', 'appdata')
console.log(settings.theme) // 'dark'
```
### Use Cases
- Global app configuration
- Organization-wide settings
- Shared templates or presets
- Feature flags
- API keys (encrypted)
### Example: App Configuration
```javascript
import { store, retrieve, showToast } from '@teachfloor/extension-kit'
// Save configuration
async function saveAppConfig(config) {
try {
await store('app-config', config, 'appdata')
showToast('Configuration saved', { color: 'green' })
} catch (error) {
console.error('Failed to save:', error)
showToast('Failed to save configuration', { color: 'red' })
}
}
// Load configuration
async function loadAppConfig() {
try {
const config = await retrieve('app-config', 'appdata')
return config || getDefaultConfig()
} catch (error) {
console.error('Failed to load:', error)
return getDefaultConfig()
}
}
// Usage
await saveAppConfig({
apiEndpoint: 'https://api.example.com',
maxRetries: 3,
timeout: 5000
})
const config = await loadAppConfig()
```
## User Data Storage
Store data specific to individual users.
### Permissions Required
**Read and Write**:
```json
{
"permissions": [
{
"permission": "userdata:write",
"purpose": "Save and load your personal preferences and app data"
}
]
}
```
:::info
`userdata:write` includes read access.
:::
**Read-Only**:
```json
{
"permissions": [
{
"permission": "userdata:read",
"purpose": "Load your personal preferences and app data"
}
]
}
```
### Usage
```javascript
import { store, retrieve } from '@teachfloor/extension-kit'
// Store user-specific data
await store('preferences', {
theme: 'light',
fontSize: 14,
sidebarCollapsed: false
}, 'userdata')
// Retrieve user-specific data
const prefs = await retrieve('preferences', 'userdata')
console.log(prefs.theme) // 'light'
```
### Use Cases
- User preferences
- Personal settings
- User state (last viewed page, filters)
- User-specific configurations
- Draft content
### Example: User Preferences
```javascript
import { store, retrieve } from '@teachfloor/extension-kit'
class PreferencesManager {
constructor() {
this.defaults = {
theme: 'light',
fontSize: 14,
notifications: true,
autoSave: true
}
}
async load() {
try {
const prefs = await retrieve('user-preferences', 'userdata')
return { ...this.defaults, ...prefs }
} catch (error) {
console.error('Failed to load preferences:', error)
return this.defaults
}
}
async save(preferences) {
try {
await store('user-preferences', preferences, 'userdata')
return true
} catch (error) {
console.error('Failed to save preferences:', error)
return false
}
}
async update(key, value) {
const prefs = await this.load()
prefs[key] = value
return this.save(prefs)
}
}
// Usage
const prefsManager = new PreferencesManager()
// Load preferences
const prefs = await prefsManager.load()
// Update a preference
await prefsManager.update('theme', 'dark')
// Save all preferences
await prefsManager.save({
theme: 'dark',
fontSize: 16,
notifications: false
})
```
## Storage Manager (recommended)
The Storage Manager (`createStorage`) is the recommended way to work with App Data and User Data. It's a thin wrapper over `store` / `retrieve` that adds three things:
1. **Namespaced keys** — every operation is scoped under a `baseKey` prefix so different features of your app can hold their own storage instances without key collisions.
2. **TTL / expiry** — pass `{ ttl: seconds }` on `set()` to auto-expire values.
3. **`query()`** — paged iteration across many rows in the namespace with a small filter + sort DSL. Available in kit `1.29.0+`.
### Permissions Required
Same as App Data / User Data — the Storage Manager doesn't add its own permissions. Pass `{ source: 'appdata' }` for org-shared storage or `{ source: 'userdata' }` for per-user storage.
```json
{
"permissions": [
{
"permission": "userdata:write",
"purpose": "Save and load user notes"
}
]
}
```
### Basic Usage
```javascript
import { createStorage } from '@teachfloor/extension-kit'
// Per-user lesson notes, all keys automatically prefixed with 'lesson-notes:'
const notes = createStorage('lesson-notes', { source: 'userdata' })
// set → row key: 'lesson-notes:lesson-42-note-1'
await notes.set('lesson-42-note-1', {
content: 'Mitochondria produce ATP via oxidative phosphorylation',
lesson_id: 'lesson-42',
tag: 'lecture',
})
// get by sub-key (namespace stripped from returned key)
const note = await notes.get('lesson-42-note-1')
// → { content: 'Mitochondria produce ATP via oxidative phosphorylation',
// lesson_id: 'lesson-42', tag: 'lecture' }
// TTL — auto-expires after 3600 seconds
// (e.g. flag a lesson as "recently viewed" for one hour)
await notes.set('recently-viewed:lesson-42', true, { ttl: 3600 })
// remove
await notes.remove('lesson-42-note-1')
```
The kit prepends the `baseKey` to every operation, so callers only ever see un-namespaced sub-keys. Different `createStorage(...)` instances can't accidentally reach each other's data.
### Query — paged filter + sort
`query({ where, sort, limit, after })` returns `{ items, nextCursor }` where each item is `{ key, value, created_at, updated_at }`. `key` is the sub-key (namespace stripped).
```javascript
// Simplest — first page of the namespace, newest first
const page = await notes.query()
// → {
// items: [
// { key: 'lesson-73-note-2', value: {...}, created_at: '...', updated_at: '...' },
// { key: 'lesson-73-note-1', value: {...}, created_at: '...', updated_at: '...' },
// { key: 'lesson-42-note-1', value: {...}, created_at: '...', updated_at: '...' },
// ...
// ],
// nextCursor: '...' | null
// }
// Pagination — resume with `after: nextCursor`
let cursor = null
do {
const p = await notes.query({ limit: 20, after: cursor })
render(p.items)
cursor = p.nextCursor
} while (cursor)
```
#### DSL
Filters run against **metadata columns only** — the `value` column is encrypted at rest and can't be predicated on. Expired rows are always excluded (no escape).
**Field / op matrix:**
| Field | Operators |
|---|---|
| `key` | `=`, `!=`, `in`, `not in`, `contains`, `not contains` |
| `created_at`, `updated_at` | `>`, `>=`, `<`, `<=` |
**Sort fields:** `updated_at`, `created_at`. **Sort directions:** `asc`, `desc`. Default when omitted: `[['updated_at', 'desc']]`.
**Predicates** are `[field, op, value]` tuples. Multiple tuples in a `where` array AND together implicitly. For OR, wrap in a `{ or: [...] }` group; for explicit AND groups, use `{ and: [...] }`. Groups nest arbitrarily.
```javascript
// All notes for lesson 42 (key sub-namespace)
await notes.query({
where: [['key', 'contains', 'lesson-42-']],
})
// Batch fetch by known ids (max 100 values per in / not in)
await notes.query({
where: [['key', 'in', ['lesson-42-note-1', 'lesson-73-note-2']]],
})
// Recent notes only, oldest first — e.g. review what you took this week
await notes.query({
where: [['updated_at', '>=', '2026-07-01']],
sort: [['updated_at', 'asc']],
})
// Combined AND — notes for lesson 42, updated since July 1
await notes.query({
where: [
['key', 'contains', 'lesson-42-'],
['updated_at', '>=', '2026-07-01'],
],
})
// Nested OR — recent notes from module 3 OR any pinned exam-prep card
await notes.query({
where: [
{ or: [
{ and: [
['key', 'contains', 'module-3-'],
['updated_at', '>=', '2026-07-01'],
]},
['key', 'in', ['exam-prep:cell-biology', 'exam-prep:genetics']],
]},
],
sort: [['updated_at', 'desc']],
limit: 20,
})
```
For `key` exact-match ops (`=`, `!=`, `in`, `not in`), values are treated as sub-keys and namespaced automatically — you write un-namespaced sub-keys, matching `get()` / `set()` semantics. `contains` / `not contains` values pass through as raw substrings and search only within the current namespace.
#### Constraints and errors
- **`in` / `not in` cap** — max 100 values per predicate. Larger arrays throw `storage.query: "in" cannot accept more than 100 values (got N)`. Split into multiple pages instead.
- **Result size cap** — server hard cap is 200 rows per page regardless of `limit`.
- **Fail-loud validation** — unsupported fields or operators throw at the call site before any RPC. Example: `where: [['value', '=', 'x']]` throws `storage.query: unsupported field "value"`.
- **Cursor is opaque** — pass the exact string back in `after`. Don't decode / hand-craft.
- **Cursor is tied to the sort order it was minted with** — if you change `sort` between pages, the cursor becomes semantically wrong (no error, but rows may be skipped or duplicated).
### Example: Lesson notes with load-more search
A learner's notes browser — filter by lesson (`lesson-42-`, `lesson-73-`, …), paginate through everything.
```jsx
import React, { useEffect, useState } from 'react'
import { createStorage } from '@teachfloor/extension-kit'
const notes = createStorage('lesson-notes', { source: 'userdata' })
function LessonNotesList() {
const [items, setItems] = useState([])
const [cursor, setCursor] = useState(null)
const [lessonFilter, setLessonFilter] = useState('') // e.g. 'lesson-42-'
const loadMore = async (reset = false) => {
const page = await notes.query({
where: lessonFilter ? [['key', 'contains', lessonFilter]] : [],
limit: 20,
after: reset ? null : cursor,
})
setItems(reset ? page.items : [...items, ...page.items])
setCursor(page.nextCursor)
}
useEffect(() => { loadMore(true) }, [lessonFilter])
return (
<>
setLessonFilter(e.target.value)}
/>
{items.map((r) => )}
{cursor && }
>
)
}
```
Storage Manager vs raw `store` / `retrieve`: use raw primitives only for a small, well-known set of keys (like a single `'config'` blob). If you're storing multiple items and might want to enumerate or filter them, use Storage Manager from the start.
## User Collection Storage
Store lists of data items for a user, with pagination support.
### Permissions Required
**Read and Write**:
```json
{
"permissions": [
{
"permission": "usercollection:write",
"purpose": "Save and load your activity history and saved items"
}
]
}
```
**Note**: `usercollection:write` includes read access.
**Read-Only**:
```json
{
"permissions": [
{
"permission": "usercollection:read",
"purpose": "Load your activity history and saved items"
}
]
}
```
### Usage
Collections allow you to store multiple items under the same key and retrieve them with pagination.
```javascript
import { createCollection } from '@teachfloor/extension-kit'
// Create a collection manager
const notes = createCollection('user-notes', { limit: 15 })
// Add items to the collection
await notes.add({
title: 'My Note',
content: 'Note content',
createdAt: Date.now()
})
await notes.add({
title: 'Another Note',
content: 'More content',
createdAt: Date.now()
})
// List items (first page)
const page1 = await notes.list()
console.log(page1.items) // Array of collection records
console.log(page1.items[0].value) // Your actual data
console.log(page1.items[0].id) // Database record ID
console.log(page1.hasMore) // true if more pages exist
console.log(page1.nextCursor) // Cursor for next page
// Load next page
if (page1.hasMore) {
const page2 = await notes.list({ cursor: page1.nextCursor })
}
// Update an existing item
const itemId = page1.items[0].id
await notes.update(itemId, {
title: 'Updated Title',
content: 'Updated content',
updatedAt: Date.now()
})
// Remove an item
await notes.remove(itemId)
// Get all items (auto-pagination)
const allNotes = await notes.getAll()
```
### Pagination
```javascript
import { createCollection } from '@teachfloor/extension-kit'
const messages = createCollection('chat-messages', { limit: 20 })
// Manual pagination
const page1 = await messages.list()
console.log(page1.items) // First 20 collection records
console.log(page1.items[0].value) // First item's data
console.log(page1.hasMore) // true if more exist
// Load next page
if (page1.hasMore) {
const page2 = await messages.list({ cursor: page1.nextCursor })
}
// Auto-pagination (get all items)
const allMessages = await messages.getAll()
```
### Use Cases
- Activity logs
- User notes or annotations
- Saved items or bookmarks
- History or timeline data
- Multi-entry forms
### Example: Chat Messages
Complete example using the Collection Manager API:
```javascript
import { createCollection, showToast } from '@teachfloor/extension-kit'
import { useState, useEffect } from 'react'
function ChatApp() {
const [messages, setMessages] = useState([])
const [isLoading, setIsLoading] = useState(false)
const [nextCursor, setNextCursor] = useState(null)
// Create collection manager
const chatMessages = createCollection('chat-messages', { limit: 15 })
// Load initial messages
useEffect(() => {
loadMessages()
}, [])
const loadMessages = async (cursor = null) => {
setIsLoading(true)
try {
const page = await chatMessages.list({ cursor })
// Extract the .value from each item
const items = page.items.map(item => item.value)
setMessages(prev => cursor ? [...prev, ...items] : items)
setNextCursor(page.hasMore ? page.nextCursor : null)
} catch (error) {
console.error('Failed to load messages:', error)
showToast('Failed to load messages', { type: 'error' })
} finally {
setIsLoading(false)
}
}
const sendMessage = async (text) => {
try {
const message = {
role: 'user',
text,
timestamp: Date.now()
}
await chatMessages.add(message)
// Optimistically add to UI
setMessages(prev => [message, ...prev])
showToast('Message sent', { type: 'success' })
} catch (error) {
console.error('Failed to send message:', error)
showToast('Failed to send message', { type: 'error' })
}
}
const editMessage = async (itemId, newText) => {
try {
const page = await chatMessages.list()
const item = page.items.find(i => i.id === itemId)
if (item) {
await chatMessages.update(itemId, {
...item.value,
text: newText,
edited: true,
editedAt: Date.now()
})
// Update in UI
setMessages(prev => prev.map(m =>
m.id === itemId ? { ...item.value, text: newText, edited: true } : m
))
showToast('Message updated', { type: 'success' })
}
} catch (error) {
console.error('Failed to update message:', error)
showToast('Failed to update message', { type: 'error' })
}
}
const deleteMessage = async (itemId) => {
try {
await chatMessages.remove(itemId)
// Remove from UI
setMessages(prev => prev.filter(m => m.id !== itemId))
showToast('Message deleted', { type: 'success' })
} catch (error) {
console.error('Failed to delete message:', error)
showToast('Failed to delete message', { type: 'error' })
}
}
return (
{messages.map((msg, i) => (
{msg.text}
))}
{nextCursor && (
)}
)
}
```
### Updating Collection Items
You can update existing collection items by their ID:
```javascript
import { createCollection, showToast } from '@teachfloor/extension-kit'
const notes = createCollection('user-notes')
async function updateNote(noteId, updates) {
try {
// Get the current item
const page = await notes.list()
const note = page.items.find(item => item.id === noteId)
if (!note) {
showToast('Note not found', { type: 'error' })
return
}
// Update with merged data
await notes.update(noteId, {
...note.value,
...updates,
updatedAt: Date.now()
})
showToast('Note updated successfully', { type: 'success' })
} catch (error) {
console.error('Update failed:', error)
showToast('Failed to update note', { type: 'error' })
}
}
// Usage
await updateNote('123', {
title: 'Updated Title',
content: 'Updated content'
})
```
**Important**:
- Requires `usercollection:write` permission
- Item ID comes from `item.id` when listing items
- Update replaces the entire value - merge with existing data if needed
- Returns the updated value
### Removing Collection Items
You can delete collection items by their ID:
```javascript
import { createCollection, showToast } from '@teachfloor/extension-kit'
const bookmarks = createCollection('saved-bookmarks')
async function removeBookmark(bookmarkId) {
try {
await bookmarks.remove(bookmarkId)
showToast('Bookmark removed', { type: 'success' })
return true
} catch (error) {
console.error('Delete failed:', error)
showToast('Failed to remove bookmark', { type: 'error' })
return false
}
}
// Usage
const page = await bookmarks.list()
const itemToDelete = page.items[0]
await removeBookmark(itemToDelete.id)
```
**Important**:
- Requires `usercollection:write` permission
- Item ID comes from `item.id` when listing items
- Delete operations are permanent
- Returns `null` on success
### Complete CRUD Example
Here's a complete example showing create, read, update, and delete operations:
```javascript
import { createCollection, showToast } from '@teachfloor/extension-kit'
import { useState, useEffect } from 'react'
function NotesManager() {
const [notes, setNotes] = useState([])
const notesCollection = createCollection('user-notes', { limit: 20 })
// Create
const addNote = async (title, content) => {
try {
await notesCollection.add({
title,
content,
createdAt: Date.now()
})
await loadNotes() // Refresh list
showToast('Note added', { type: 'success' })
} catch (error) {
showToast('Failed to add note', { type: 'error' })
}
}
// Read
const loadNotes = async () => {
try {
const page = await notesCollection.list()
setNotes(page.items)
} catch (error) {
showToast('Failed to load notes', { type: 'error' })
}
}
// Update
const updateNote = async (noteId, updates) => {
try {
const note = notes.find(n => n.id === noteId)
await notesCollection.update(noteId, {
...note.value,
...updates,
updatedAt: Date.now()
})
await loadNotes() // Refresh list
showToast('Note updated', { type: 'success' })
} catch (error) {
showToast('Failed to update note', { type: 'error' })
}
}
// Delete
const deleteNote = async (noteId) => {
try {
await notesCollection.remove(noteId)
setNotes(prev => prev.filter(n => n.id !== noteId))
showToast('Note deleted', { type: 'success' })
} catch (error) {
showToast('Failed to delete note', { type: 'error' })
}
}
useEffect(() => {
loadNotes()
}, [])
return (
{notes.map(note => (
{note.value.title}
{note.value.content}
))}
)
}
```
## Data Types
All storage methods automatically handle serialization:
### Supported Types
```javascript
// String
await store('name', 'John Doe', 'userdata')
// Number
await store('count', 42, 'userdata')
// Boolean
await store('enabled', true, 'userdata')
// Object
await store('settings', { theme: 'dark', lang: 'en' }, 'userdata')
// Array
await store('items', [1, 2, 3, 4, 5], 'userdata')
// Nested Objects
await store('config', {
ui: { theme: 'dark' },
features: { beta: true },
limits: { max: 100 }
}, 'appdata')
```
### Type Handling
```javascript
// Data is automatically serialized and deserialized
const settings = await retrieve('settings', 'userdata')
// No need to JSON.parse - objects are returned as objects
console.log(settings.theme) // Direct property access
// Arrays remain arrays
const items = await retrieve('items', 'userdata')
items.forEach(item => console.log(item))
```
## Security
All data stored through the Extension Kit is **automatically encrypted at rest** on the Teachfloor platform.
**Safe to store**:
- User preferences and settings
- App configurations
- UI state and draft content
- Non-sensitive user data
- Cached public data
- API keys for third-party services
**Do not store**:
- User passwords
- Credit card numbers or payment information
- Social security numbers or national IDs
- Private encryption keys
- Data belonging to other users
## Next Steps
→ Continue to [Realtime Channels](/docs/apps/advanced-topics/realtime)
## Additional Resources
- [Best Practices](/docs/apps/references/best-practices) - Storage patterns, error handling, caching, and performance
- [Permissions](/docs/apps/advanced-topics/permissions) - Storage permission requirements
- [Extension Kit Integration](/docs/apps/core-concepts/extension-kit/integration)
---
## Document: /docs/apps/advanced-topics/realtime
URL: /docs/apps/advanced-topics/realtime
# Realtime Channels
Live event streams scoped to a course or user. Extension apps subscribe to a channel, receive any events that other clients publish on it, and can publish their own.
:::info
**Requires `@teachfloor/extension-kit` ≥ 1.22.0.** Earlier versions of the kit don't ship the `realtime` namespace. Bump your app's dependency before adding the `realtime` permission to your manifest.
:::
## What you can build
- Live cohort presence — *"3 learners are also on this course right now"*.
- Multiplayer review — flashcards / quizzes / polls with synchronized state.
- Live admin dashboards — *"new submission received"*, *"X just completed the module"*.
## Channel model
A channel is identified by a **scope** and a **resource id**. Two scopes are available:
| Scope | Resource id | Who can subscribe |
|---|---|---|
| `course` | The id of a course in the user's organization. | Any member of that course's organization who has the app installed with the `realtime` permission. |
| `user` | The current user's id. | Only the user themselves. |
The resource id is the same id you receive from `useExtensionContext()` — for example `environment.context.course.id` on a course viewport. You never construct the channel string yourself; the SDK does it for you.
Channels are **private**: Teachfloor authenticates every subscription server-side and enforces the rule in the table above before accepting it.
## Permissions
Add the `realtime` permission to your manifest:
```json
{
"permissions": [
{
"permission": "realtime",
"purpose": "Show live cohort presence and broadcast quiz answers to peers in real time"
}
]
}
```
The `realtime` permission grants:
- Subscribing to `course`-scoped channels for any course in an organization the user is a member of.
- Subscribing to your own `user`-scoped channel (`scope: 'user'`, `id: `).
- Publishing to channels you're subscribed to.
It does **not** grant cross-app, cross-org, or other-user channel access.
:::info
**Resource ids are gated by their own read permissions.** Subscribing to a `course`-scoped channel needs `courses:read` in addition to `realtime` — without it the host strips `course` from the viewport payload, so `environment.context.course.id` is `undefined` and there's no id to subscribe with. The same applies for `modules:read` and `elements:read` if you ever derive an id from those contexts.
:::
## SDK API
```js
import { realtime } from '@teachfloor/extension-kit'
// Subscribe
const channel = realtime.subscribe({
scope: 'course',
id: courseId,
})
// Listen to a specific event
const off = channel.on('card_added', (payload) => {
console.log('new card from a peer:', payload.data)
console.log('sent by user:', payload.fromUserId)
console.log('at:', payload.at)
})
// Or listen to every event on the channel
channel.onAny((eventName, payload) => { /* ... */ })
// Publish
channel.publish('card_added', { cardId: 'abc', front: '...', back: '...' })
// Clean up
off() // remove a single listener
channel.unsubscribe() // tear down the entire subscription
```
### Event payload shape
What your `on` handler receives:
```js
{
data: { /* what the publisher passed to .publish() */ },
fromUserId: 'QnX4ogW9QbyLpEz9', // publisher's id (same shape as userContext.id)
at: '2026-06-28T03:24:11+02:00', // server timestamp
}
```
`fromUserId` is the publisher's id — the same value you get from `useExtensionContext().userContext.id`. You can compare it against your own user id to filter your own activity, or use it in a navigation helper like `goToPath('/${slug}/users/${fromUserId}')` to deep-link to the peer's profile.
### Notes
- Publishers do **not** receive their own events back. Update your own UI optimistically when you call `.publish()`.
- Subscription and publish failures (wrong scope, missing permission, rate limit hit, network blip) are delivered to the channel's `onError(…)` handler — register one to log or surface them. Without it, failures are dropped.
- There's no message history. If a learner joins after an event was published, they won't see it. Cache state in `appdata` / `userdata` if you need persistence.
## Limits
| Limit | Value |
|---|---|
| Event name | `^[a-z][a-z0-9_]*$`, max 64 chars |
| Payload size | 4 KB per published message |
| Per user, per app | 60 messages / minute |
| Per app (org-wide) | 600 messages / minute |
| Concurrent subscriptions per app instance | unlimited (but bounded by the host's single WebSocket) |
Rate limits return `429` with a `retry_after_seconds` field; the SDK surfaces them through `channel.onError(…)` with `code: 'rate_limited'`.
## Example: live "currently here" counter
Realtime channels don't keep a presence roster for you — building one is a pattern of three pieces: announce yourself, refresh the announcement on a heartbeat, and locally drop peers you haven't heard from in a while. The example below shows all three.
```jsx
import { useEffect, useState } from 'react'
import { realtime, useExtensionContext } from '@teachfloor/extension-kit'
const HEARTBEAT_MS = 30_000 // re-announce every 30s
const STALE_AFTER = 60_000 // drop peers we haven't heard from in 60s
const PRUNE_TICK_MS = 10_000 // re-check the stale window every 10s
const LiveHereCounter = () => {
const { environment } = useExtensionContext()
const [peers, setPeers] = useState({}) // { [userId]: lastSeen timestamp }
useEffect(() => {
const courseId = environment?.context?.course?.id
if (!courseId) return
const channel = realtime.subscribe({ scope: 'course', id: courseId })
// Add a peer or refresh their lastSeen on every heartbeat.
channel.on('joined', ({ fromUserId }) => {
setPeers((prev) => ({ ...prev, [fromUserId]: Date.now() }))
})
// Drop peers who explicitly leave (covers tab close on unmount).
channel.on('left', ({ fromUserId }) => {
setPeers((prev) => {
const next = { ...prev }
delete next[fromUserId]
return next
})
})
// Announce ourselves immediately, then on every heartbeat.
const announce = () => channel.publish('joined', {})
announce()
const heartbeat = setInterval(announce, HEARTBEAT_MS)
// Locally prune peers who've gone silent — covers closed laptops
// and network drops where no `left` is fired.
const prune = setInterval(() => {
const cutoff = Date.now() - STALE_AFTER
setPeers((prev) => Object.fromEntries(
Object.entries(prev).filter(([, lastSeen]) => lastSeen >= cutoff)
))
}, PRUNE_TICK_MS)
return () => {
clearInterval(heartbeat)
clearInterval(prune)
try { channel.publish('left', {}) } catch (_) { /* unsubscribed already */ }
channel.unsubscribe()
}
}, [environment])
// +1 for the current user — the publisher never receives their own events.
return
{Object.keys(peers).length + 1} here right now
}
```
The numbers come from a few design choices worth thinking through for your own app:
- **Heartbeat interval** trades freshness against publish budget. At 30 seconds, each learner uses 2 of their 60-events-per-minute budget — leaves plenty of headroom for other event types.
- **Stale window** is set to two missed heartbeats so a transient network blip doesn't drop someone for a beat.
- **Prune tick** controls how smoothly peers fade out in the UI; smaller values feel snappier but cost a tiny bit more re-rendering.
## Security model
Permission and scope checks run server-side on **both** the subscribe and publish paths. Even if a client bypassed the SDK and called the underlying API directly, it would still hit the same checks:
- The app must be installed in the resource's organization and declare the `realtime` permission.
- The user must have access to the underlying resource (member of the course's org for `course` channels; only their own user id for `user` channels).
- Publish payloads are validated against the size and rate limits above.
There is no way for an app to read a channel it doesn't subscribe to, or to publish on behalf of another user.
## Next Steps
→ Continue to [Webhooks](/docs/apps/advanced-topics/webhooks)
## Additional Resources
- [Webhooks](/docs/apps/advanced-topics/webhooks) - Signed HTTP deliveries to your app's backend. Complementary to realtime — reaches your server even when no learner is online.
- [Permissions](/docs/apps/advanced-topics/permissions) - The full `realtime` permission listing alongside the rest of the platform's permissions.
- [Data Storage](/docs/apps/advanced-topics/data-storage) - Pair realtime with `appdata` / `userdata` for state that survives reloads.
---
## Document: /docs/apps/advanced-topics/webhooks
URL: /docs/apps/advanced-topics/webhooks
# Webhooks
Webhooks deliver platform events to your app's **own backend** — HTTP POSTs signed with your app's secret, fired whenever something the app cares about happens on the platform. Complements [Realtime Channels](/docs/apps/advanced-topics/realtime): realtime pushes events into your app's SDK view while a learner is active; webhooks reach your server even when no learner is online.
## What you can build
- **Data sync** — mirror course completions, quiz submissions, or member joins into an HR system, LMS, analytics warehouse, or CRM.
- **Backend integrations** — post activity notifications to Slack, Discord, Microsoft Teams, or a shared inbox.
- **Provisioning + teardown** — bootstrap per-tenant state when your app is installed on a new organization; clean up when it's uninstalled.
- **Third-party mirroring** — keep a system-of-record (e.g. a marketing tool or student-info system) in step with events happening on the platform.
## How it works
Every extension app can optionally declare a single **webhook endpoint URL** in its manifest. When an organization installs the app, Teachfloor auto-provisions the delivery pipeline against that URL — no per-tenant configuration required. The app's backend receives POSTs for every event it subscribed to, plus two lifecycle events (`app.installed`, `app.uninstalled`) that fire regardless of subscription.
One URL per app is delivered to for **every** installing organization. The payload carries the organization identifier so your backend can attribute the event to the right tenant.
## Manifest schema
Declare the block at the top level of your manifest alongside `views` and `permissions`:
```json
{
"id": "my-app",
"name": "My App",
"version": "1.0.0",
"webhook": {
"url": "https://my-app.com/teachfloor/webhook",
"events": [
"course.completed",
"element.completed",
"course.join"
]
}
}
```
**Fields:**
| Field | Required | Rules |
|---|---|---|
| `webhook.url` | yes (when block present) | `https://` only. HTTP is rejected — signed payloads over plaintext give no meaningful integrity guarantee. Max 2048 chars. |
| `webhook.events` | no | Array of event names drawn from the [event catalog](#event-catalog). Empty array is valid — you still receive the lifecycle events. Unknown event names are rejected at manifest push time. |
Omit the block entirely if your app doesn't need backend deliveries.
## The signing secret
Every app has a single **master signing secret** used to sign every webhook delivery to your endpoint, across every installing organization. You manage **one secret** per app, not one per install.
**Where to find it:** Developers → Apps → your app → the header metadata strip includes a "Signing Secret" cell with a reveal button. Click to show the raw value.
The secret is generated automatically when your app is created.
## Signature verification
Every delivery includes a `Teachfloor-Signature` header. The signature is `HMAC-SHA256(secret, request_body)`. Verify it before trusting the payload.
**Node.js / Express:**
```javascript
const crypto = require('crypto')
const express = require('express')
const app = express()
// Capture the raw body — Express's default JSON middleware discards it.
app.use('/teachfloor/webhook', express.raw({ type: 'application/json' }))
const TEACHFLOOR_SECRET = process.env.TEACHFLOOR_WEBHOOK_SECRET
app.post('/teachfloor/webhook', (req, res) => {
const signature = req.header('Teachfloor-Signature')
const expected = crypto
.createHmac('sha256', TEACHFLOOR_SECRET)
.update(req.body)
.digest('hex')
if (!signature || !crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send('invalid signature')
}
const event = JSON.parse(req.body.toString())
console.log('received', event.type, 'for', event.data.organization.id)
res.status(200).end()
})
```
**Python / Flask:**
```python
import hmac
import hashlib
import json
import os
from flask import Flask, request, abort
app = Flask(__name__)
TEACHFLOOR_SECRET = os.environ["TEACHFLOOR_WEBHOOK_SECRET"].encode()
@app.route("/teachfloor/webhook", methods=["POST"])
def webhook():
signature = request.headers.get("Teachfloor-Signature", "")
expected = hmac.new(TEACHFLOOR_SECRET, request.data, hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature, expected):
abort(401)
event = json.loads(request.data)
print(f"received {event['type']} for {event['data']['organization']['id']}")
return "", 200
```
**PHP:**
```php
$secret = getenv('TEACHFLOOR_WEBHOOK_SECRET');
$body = file_get_contents('php://input');
$signature = $_SERVER['HTTP_TEACHFLOOR_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $body, $secret);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit;
}
$event = json_decode($body, true);
error_log("received {$event['type']} for {$event['data']['organization']['id']}");
http_response_code(200);
```
Always use a constant-time comparison (`timingSafeEqual` / `hmac.compare_digest` / `hash_equals`). A plain `==` opens a timing side-channel on the signature check.
## Payload shape
Every delivery has the same envelope, with an event-specific `data` block inside:
```json
{
"id": "evt_abc123",
"type": "course.completed",
"created_at": "2026-07-25T14:32:01.000000Z",
"installation_id": "7YWJgrMnJMybawx1",
"data": {
...event-specific fields
}
}
```
`id` is stable and unique — safe to use as an idempotency key (and mirrored in the `Teachfloor-Idempotency-Key` header — see [Delivery guarantees](#delivery-guarantees)). `type` matches one of the [subscribable event names](#event-catalog). `installation_id` is the hashid of the InstalledApp this delivery targets — use it to look up which install fired the event without inspecting the `data` block (whose shape varies per event type).
### Lifecycle events
Two events are always delivered to your endpoint regardless of your `events` subscription:
**`app.installed`** — fired when an organization installs your app.
```json
{
"id": "evt_...",
"type": "app.installed",
"created_at": "2026-07-25T14:32:01.000000Z",
"installation_id": "7YWJgrMnJMybawx1",
"data": {
"organization": {
"id": "acme-corp",
"name": "Acme Corp"
},
"app": {
"id": "6a41091c53b30",
"name": "My App",
"version": "1.0.0"
},
"installer": {
"id": "usr_abc123",
"email": "alice@acme.com",
"full_name": "Alice Doe"
},
"credentials": {
"client_id": "9f8e7d6c-...",
"access_token": "eyJhbGc...",
"refresh_token": "def50200...",
"expires_at": "2026-08-25T14:32:01+00:00",
"token_type": "Bearer",
"scope": "courses:read modules:read"
}
}
}
```
The `installer` block identifies the user who clicked Install — always present on `app.installed`, regardless of whether the app declares any additional integrations. Use it to auto-provision or match an account on your landing page before the user arrives, so the post-install redirect can drop them straight into a signed-in experience rather than a sign-up form.
`installer` fields:
- `id` — the installer's user hashid; same format the SDK surfaces elsewhere.
- `email` — key your account lookup on this.
- `full_name` — for greetings / display.
The `credentials` block is **opt-in** and only appears when both of these are true in the app manifest:
1. An explicit `oauth: { type: "install" }` block declares that the app wants install-integrated OAuth.
2. At least one declared permission maps to an OAuth scope (currently: `courses:read`, `modules:read`, `elements:read`, `members:read`, `activities:read`).
See the [OAuth chapter](/docs/apps/advanced-topics/oauth) for the full end-to-end recipe — opt-in, storage shape, calling the API, refresh flow, and code samples in Node / Python / PHP.
```json
{
"id": "my-app",
"oauth": { "type": "install" },
"permissions": [
{ "permission": "courses:read", "purpose": "Sync course completions to our HR tool" }
],
"webhook": { "url": "https://…", "events": ["course.completed"] }
}
```
Without the `oauth` block, no tokens are minted even if the manifest declares OAuth-mappable permissions — an SDK-only app that adds `courses:read` for in-app use won't unexpectedly start receiving credentials in its webhook receiver logs. Adding `oauth: { type: "install" }` is the explicit opt-in.
SDK-only apps and apps with only SDK-scoped storage permissions receive no `credentials` — they can still deliver a signed webhook, they just don't get an access token for the public API. When `credentials` is present, use the tokens to call the public API on behalf of the installing organization — bearer-authenticated with `access_token`, refreshable via the standard OAuth 2.0 `/oauth/token` refresh flow using `client_id` + `client_secret` + `refresh_token`. Store `access_token` and `refresh_token` per-install (keyed by `organization.id`).
The `client_secret` is **not** in the webhook payload — you fetch it once from the Teachfloor developer dashboard (Developers → Apps → your app → OAuth Client Secret) or from the `teachfloor apps create` CLI response, and store it in your app config. It's the same across every install of your app.
`credentials` fields:
- `client_id` — the app's OAuth client identifier. Same across every install; not sensitive. Delivered here for correlation.
- `access_token` — bearer token for public API calls. Expires per `expires_at`.
- `refresh_token` — POST to `/oauth/token` with `grant_type=refresh_token` to mint a new access token; refresh tokens rotate (the old one is invalidated).
- `expires_at` — ISO 8601 timestamp for the access token; refresh before this.
- `token_type` — always `Bearer`.
- `scope` — space-separated scopes granted to this token.
**`app.uninstalled`** — fired just before an organization uninstalls your app.
Same payload shape as `app.installed` **minus the `installer` block**. `app.version` reflects the version being uninstalled (relevant during upgrades — see below). Use this event to purge tenant state.
### Version upgrades
When an organization upgrades your app to a new version, the platform tears down the old install and creates a new one. Your backend receives, in order:
1. `app.uninstalled` for the old version (fired against the old endpoint URL if that changed between versions).
2. `app.installed` for the new version (fired against the new endpoint URL).
Design your handlers to be idempotent. The envelope-level `installation_id` changes between the old and new install; correlate by `organization.id` if you need to detect an upgrade rather than a fresh install.
If the new manifest **drops** the `webhook` block, only `app.uninstalled` fires — the app.installed event has nowhere to go. Treat the `app.uninstalled` as your last signal from that org.
## Event catalog
You can subscribe to any of the following:
| Event | Fires when |
|---|---|
| `course.created` | A course is created in the org |
| `course.updated` | Course metadata changes |
| `course.completed` | A learner marks a course as completed |
| `course.join` | A learner joins a course |
| `module.created` | A module is added to a course |
| `module.updated` | Module metadata changes |
| `element.created` | An element is added to a module |
| `element.updated` | Element metadata changes |
| `element.deleted` | An element is removed |
| `element.completed` | A learner completes an element |
| `member.login` | A member logs into the org |
| `app.installed` | (always delivered) |
| `app.uninstalled` | (always delivered) |
Subscribing to an unknown event name is rejected at manifest push time.
## Delivery guarantees
- **Retry** — up to 3 attempts on any non-2xx response, with exponential backoff between attempts.
- **Timeout** — 10 seconds per attempt. Endpoints that take longer are considered failed and retried. If you need to do more work than 10 seconds allows, respond 2xx immediately and enqueue the work.
- **Order** — deliveries are queued and processed asynchronously. Events for the same organization may be delivered out of order if one takes multiple attempts to succeed. Use the envelope's `created_at` if strict ordering matters.
- **At-least-once** — a single event may be delivered more than once if your endpoint responds 200 after our timeout window. Dedupe on the `Teachfloor-Idempotency-Key` header (equivalent to the envelope `id`) — the header is stable across retry attempts, so it lets you short-circuit duplicate work before parsing the body.
- **HTTPS required** — HTTP endpoints are rejected at manifest validation. TLS 1.2+ is expected on your endpoint.
Respond with any 2xx status code to acknowledge receipt. Any other response (4xx, 5xx, timeout) triggers a retry.
## Security model
- **Signature** verification is your responsibility. A delivery that doesn't verify against your master secret is forged — reject it.
- **The delivery URL and secret cross the wire only over HTTPS**, both from admin to browser (when revealing the secret) and from Teachfloor to your endpoint.
- **No inbound path is opened by declaring webhooks.** Webhooks are strictly outbound — Teachfloor never calls into your app's SDK view or your platform-side runtime as a result of a webhook declaration.
- **Uninstall removes the endpoint.** When an org uninstalls your app, the webhook endpoint row is deleted (after the final `app.uninstalled` delivery). Further platform events do not attempt to reach your endpoint for that org.
## Next Steps
→ Continue to [OAuth](/docs/apps/advanced-topics/oauth)
## Additional Resources
- [Realtime Channels](/docs/apps/advanced-topics/realtime) - Sub-second event delivery into your app's SDK view while the learner is active. Complementary to webhooks (which reach your backend regardless).
- [Permissions](/docs/apps/advanced-topics/permissions) - The permission scopes your app can declare in its manifest.
- [App Manifest](/docs/apps/core-concepts/app-manifest) - The full manifest schema, including the `webhook` block.
---
## Document: /docs/apps/advanced-topics/oauth
URL: /docs/apps/advanced-topics/oauth
# OAuth
Extension apps get two independent OAuth capabilities on a single app registration — pick either or both depending on what your app needs:
- **`install`** — install-integrated OAuth. Every install auto-mints an org-scoped access token + refresh token, delivered to your backend inside the `app.installed` webhook. No browser step; the Install click IS the grant. Use this for background integrations that call the Teachfloor API without any specific user in the loop (nightly sync, event-triggered updates, admin dashboards).
- **`authorize`** — classic OAuth 2.0 authorization-code flow. A user signs in via Teachfloor's `/oauth/authorize`, your app receives a code, exchanges it for tokens. Add the `openid` scope and it becomes full OpenID Connect (id_token identifying the user) — perfect for "Sign in with Teachfloor" flows in your app's UI.
Both flows share the SAME `client_id` + `client_secret`. Configure one, both, or none — the manifest tells Teachfloor which behaviors to enable.
This chapter covers both capabilities: opting in, receiving credentials, making API calls, refreshing tokens, silent sign-in, and revocation.
## What you can build
**With `install`:**
- **Backend integrations that read platform data** — sync course completions to your HR system, mirror members to your CRM, ingest activity streams into your data warehouse.
- **Automated workflows** — poll the API from your backend on your own schedule, augmenting the push-based webhook data.
**With `authorize`:**
- **Sign in with Teachfloor** on your own web UI — users click a button, get redirected to Teachfloor, land back on your app already signed in.
- **Silent sign-in on post-install redirect** — after a user installs your app, drop them onto your setup page already signed in (no re-authentication).
If you only need in-app data access (from within your app's dashboard view), the SDK's [`teachfloor.get(...)`](/docs/apps/core-concepts/extension-kit/integration) helpers already do that. OAuth is for anything OUTSIDE the dashboard — your backend, your own web UI, external integrations.
## How it works
**`install` flow** — When an organization installs your app, Teachfloor mints an access token + refresh token bound to that installation and delivers them inside the `app.installed` webhook payload. Your backend stores the tokens per-installation and uses them as bearer tokens against `https://api.teachfloor.com/v0/*`.
**`authorize` flow** — Your app redirects the user's browser to `https://app.teachfloor.com/oauth/authorize?client_id=...&scope=openid+profile+email&redirect_uri=...`. The user consents (or silent-approves if you use `prompt=none`), Teachfloor redirects to your callback with a code, your backend exchanges the code at `/oauth/token` for tokens including an id_token identifying the user.
One `client_id` + `client_secret` pair identifies your app across every organization that installs it; each install then gets its own `access_token` and `refresh_token`. The `client_id` + `client_secret` are shown to you once at app creation (via the CLI and the Developer dashboard) — store them in your app config; they never appear in webhook payloads.
## Opt in
The `oauth` manifest block declares which capabilities your app wants. Pick either or both:
**Install-integrated (background API access):**
```json
"oauth": { "install": true }
```
**Authorize flow with OIDC (user sign-in):**
```json
"oauth": {
"authorize": {
"redirect_uris": [
"https://my-app.com/oauth/callback",
"http://localhost:3000/oauth/callback"
]
}
}
```
**Hybrid (both — same app, same client credentials):**
```json
"oauth": {
"install": true,
"authorize": {
"redirect_uris": ["https://my-app.com/oauth/callback"]
}
}
```
Full manifest example (hybrid):
```json
{
"id": "my-app",
"name": "My App",
"version": "1.0.0",
"oauth": {
"install": true,
"authorize": {
"redirect_uris": ["https://my-app.com/oauth/callback"]
}
},
"permissions": [
{ "permission": "courses:read", "purpose": "Sync course completions to our HR tool" },
{ "permission": "modules:read", "purpose": "Attribute completions to the right module" },
{ "permission": "openid", "purpose": "Sign you in to My App using your Teachfloor account" },
{ "permission": "profile", "purpose": "Show your name on your My App dashboard" },
{ "permission": "email", "purpose": "Contact you about integration issues" }
],
"webhook": {
"url": "https://my-app.com/teachfloor/webhook",
"events": ["course.completed"]
}
}
```
**Fields:**
| Field | Required | Rules |
|---|---|---|
| `oauth.install` | no | Must be the literal boolean `true` when present. Enables install-integrated tokens delivered via webhook. |
| `oauth.authorize` | no | Object with `redirect_uris` when present. Enables browser code flow at `/oauth/authorize`. |
| `oauth.authorize.redirect_uris` | yes (when `authorize` block present) | Array of one or more `https://` callback URLs. `http://localhost` / `http://127.0.0.1` / `http://[::1]` allowed for local dev per RFC 8252. |
At least one of `install` or `authorize` must be declared when the `oauth` block is present.
### Additional requirements per capability
**For `install`:**
- A `webhook` block — credentials arrive inside the `app.installed` webhook, so if there's no webhook endpoint, there's nowhere to deliver them.
- At least one permission that maps to an OAuth scope (see the [permission mapping table](#permission-scope-mapping)).
**For `authorize`:**
- OIDC identity permissions in your `permissions` array — declare which claims your app is allowed to request. Requests for scopes not in your manifest are rejected with `invalid_scope`. Minimum for basic sign-in: `openid`. Add `profile` for the user's name, `email` for their email address.
## Permission → scope mapping
Adding a permission to the manifest allows your app to request the matching OAuth scope — no separate scope declaration needed. Permissions split into two categories by flow:
**Grantable on `install` tokens** (delivered via `app.installed` webhook, org-scoped, hit the public API):
| Manifest permission | OAuth scope | Grants |
|---|---|---|
| `courses:read` | `courses:read` | GET `/v0/courses/*` |
| `modules:read` | `modules:read` | GET `/v0/modules/*` |
| `elements:read` | `elements:read` | GET `/v0/elements/*` |
| `members:read` | `members:read` | GET `/v0/members/*`, GET `/v0/courses/{id}/members/*` |
| `activities:read` | `activities:read` | GET `/v0/activities/*`, GET `/v0/elements/{id}/activities` |
**Grantable on `authorize` tokens** (delivered via `/oauth/token` code exchange, user-scoped, populate id_token claims):
| Manifest permission | OAuth scope | Delivered claim in id_token |
|---|---|---|
| `openid` | `openid` | `sub` (stable user id), `iss`, `aud`, `iat`, `exp` (base claims — always present when openid is granted) |
| `profile` | `profile` | `name` |
| `email` | `email` | `email`, `email_verified` |
These two sets are mutually exclusive per token flow: install tokens can't carry identity claims; authorize tokens can't reach the public API. If your app needs both, declare `install: true` AND `authorize: {...}` in the manifest and use each flow for its intended purpose.
Other manifest permissions (data storage, realtime, AI, etc.) are SDK-only — they don't map to any OAuth scope and don't appear on issued tokens.
## Receiving credentials
Credentials arrive in the `data.credentials` block of the `app.installed` webhook. See the [Webhooks chapter](/docs/apps/advanced-topics/webhooks#lifecycle-events) for the full envelope.
```json
{
"type": "app.installed",
"data": {
"organization": { "id": "acme-corp", "name": "Acme Corp" },
"installer": { "id": "usr_...", "email": "alice@acme.com", "full_name": "Alice Doe" },
"credentials": {
"client_id": "9f8e7d6c-...",
"access_token": "eyJhbGc...",
"refresh_token": "def50200...",
"expires_at": "2026-08-25T14:32:01+00:00",
"token_type": "Bearer",
"scope": "courses:read modules:read"
}
}
}
```
Store `access_token` and `refresh_token` per-install (keyed by `organization.id`) — you'll need both for API calls and for the refresh flow. Combine them at refresh time with the `client_id` + `client_secret` you already have in your app config from when you created the app.
The `client_secret` is **not** in this payload. Fetch it once from the Teachfloor developer dashboard (Developers → Apps → your app → **OAuth Client Secret**) or from the `teachfloor apps create` CLI response, and store it in your app config — it's the same across every install of your app, so per-install storage isn't required.
**Recommended per-install storage shape:**
```sql
CREATE TABLE teachfloor_installations (
organization_id VARCHAR(255) PRIMARY KEY, -- from data.organization.id
installer_email VARCHAR(255), -- from data.installer.email
client_id VARCHAR(255) NOT NULL, -- from data.credentials.client_id
access_token TEXT NOT NULL,
refresh_token TEXT NOT NULL,
expires_at TIMESTAMP NOT NULL,
scopes TEXT NOT NULL,
installed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
Keep `client_secret` in your app-level config (env var, secrets manager), NOT per-install.
Treat all four values as sensitive. Don't log them to any observability tool that might retain them, and redact them from error reports.
## Auto-provisioning users on install
The `installer` block identifies the user who clicked Install — always present, whether or not the app opted into OAuth. Use it to auto-provision an account on your side BEFORE the user's browser arrives at your post-install landing page:
```javascript
// Inside your webhook handler for app.installed
async function handleAppInstalled(event) {
const { organization, installer, credentials } = event.data
// Provision or match an account for the installer.
const user = await findOrCreateUser({
email: installer.email,
name: installer.full_name,
externalId: installer.id,
})
// Store the OAuth credentials keyed by org.
if (credentials) {
await saveInstallation({
organizationId: organization.id,
installerEmail: installer.email,
...credentials,
})
}
// Optionally sign the user in via a magic link so their post-install
// redirect lands in a signed-in state.
await sendMagicLink(user, { redirectUrl: '/dashboard' })
}
```
By the time the user's browser arrives at your post-install redirect URL, their account exists on your side. Match them to the installation by email and drop them into a signed-in experience — no sign-up form.
## Install delivery timing
The `app.installed` webhook lands at your backend **before** the browser follows the post-install redirect. Your landing page can rely on the installation already being provisioned on your side (credentials stored, user auto-provisioned).
To make that guarantee your endpoint has a **3-second window** to ack. If you respond in time, the redirect fires only after your ack. If you don't (slow, down, error), the install still succeeds and the webhook is retried on the standard schedule — the redirect proceeds and your landing page falls back to your normal sign-up flow.
**Practical implication:** keep your `app.installed` handler fast. Store the credentials, provision a user, ack with 200. Anything heavy (email dispatch, external API calls, analytics) should happen in a background job after the ack.
## Making API calls
Bearer-authenticate with the `access_token`:
```bash
curl https://api.teachfloor.com/v0/courses \
-H "Authorization: Bearer eyJhbGc..."
```
Every request must include the token. The token grants access scoped to:
- **The organization that installed the app** — you can only reach that org's data.
- **The scopes declared in the token** — a token with only `courses:read` can call `GET /v0/courses/*` but gets 403 on `GET /v0/members`.
If a request returns 401, the token has expired or been revoked — refresh it. If it returns 403 `Insufficient scope`, the token doesn't have permission for that endpoint (your manifest didn't declare a mapping permission).
**Node.js:**
```javascript
const fetch = require('node-fetch')
async function getCourses(installation) {
let accessToken = installation.access_token
if (Date.parse(installation.expires_at) < Date.now() + 60_000) {
// Refresh proactively if we're within 60s of expiry.
({ access_token: accessToken } = await refreshToken(installation))
}
const res = await fetch('https://api.teachfloor.com/v0/courses', {
headers: { 'Authorization': `Bearer ${accessToken}` },
})
if (res.status === 401) {
// Expired between our check and the request — refresh once and retry.
const { access_token: fresh } = await refreshToken(installation)
return fetch('https://api.teachfloor.com/v0/courses', {
headers: { 'Authorization': `Bearer ${fresh}` },
}).then(r => r.json())
}
return res.json()
}
```
**Python:**
```python
import requests
from datetime import datetime, timezone, timedelta
def get_courses(installation):
access_token = installation["access_token"]
expires_at = datetime.fromisoformat(installation["expires_at"])
if expires_at < datetime.now(timezone.utc) + timedelta(seconds=60):
access_token = refresh_token(installation)["access_token"]
r = requests.get(
"https://api.teachfloor.com/v0/courses",
headers={"Authorization": f"Bearer {access_token}"},
)
if r.status_code == 401:
access_token = refresh_token(installation)["access_token"]
r = requests.get(
"https://api.teachfloor.com/v0/courses",
headers={"Authorization": f"Bearer {access_token}"},
)
return r.json()
```
**PHP:**
```php
function getCourses(array $installation): array {
$accessToken = $installation['access_token'];
$expiresAt = strtotime($installation['expires_at']);
if ($expiresAt < time() + 60) {
$accessToken = refreshToken($installation)['access_token'];
}
$ch = curl_init('https://api.teachfloor.com/v0/courses');
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer {$accessToken}"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code === 401) {
$accessToken = refreshToken($installation)['access_token'];
// …retry…
}
return json_decode($body, true);
}
```
## Refreshing tokens
Access tokens expire after **1 hour**. Refresh tokens are valid for **30 days** and rotate on every use — the response contains a fresh `refresh_token` that supersedes the previous one; store it immediately.
```
POST https://api.teachfloor.com/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=
&client_id=
&client_secret=
```
Response:
```json
{
"token_type": "Bearer",
"expires_in": 3600,
"access_token": "eyJhbGc...(new)...",
"refresh_token": "def50200...(new)..."
}
```
Store the new `access_token`, `refresh_token`, and the new expiry (`now() + expires_in` seconds). The old refresh token is invalidated the moment this request succeeds — using it again returns `invalid_grant`.
**Node.js:**
```javascript
async function refreshToken(installation) {
const body = new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: installation.refresh_token,
client_id: installation.client_id,
client_secret: process.env.TEACHFLOOR_CLIENT_SECRET, // from your app config
})
const res = await fetch('https://api.teachfloor.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
})
if (!res.ok) throw new Error(`refresh failed: ${res.status}`)
const { access_token, refresh_token, expires_in } = await res.json()
const expires_at = new Date(Date.now() + expires_in * 1000).toISOString()
await updateInstallation(installation.organization_id, {
access_token, refresh_token, expires_at,
})
return { access_token, refresh_token, expires_at }
}
```
**Python:**
```python
def refresh_token(installation):
r = requests.post(
"https://api.teachfloor.com/oauth/token",
data={
"grant_type": "refresh_token",
"refresh_token": installation["refresh_token"],
"client_id": installation["client_id"],
"client_secret": os.environ["TEACHFLOOR_CLIENT_SECRET"], # from your app config
},
)
r.raise_for_status()
body = r.json()
body["expires_at"] = (
datetime.now(timezone.utc) + timedelta(seconds=body["expires_in"])
).isoformat()
update_installation(installation["organization_id"], body)
return body
```
**PHP:**
```php
function refreshToken(array $installation): array {
$ch = curl_init('https://api.teachfloor.com/oauth/token');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'grant_type' => 'refresh_token',
'refresh_token' => $installation['refresh_token'],
'client_id' => $installation['client_id'],
'client_secret' => getenv('TEACHFLOOR_CLIENT_SECRET'), // from your app config
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);
$body['expires_at'] = gmdate('c', time() + $body['expires_in']);
updateInstallation($installation['organization_id'], $body);
return $body;
}
```
**Retry pattern:** on any 401 from a public API call, refresh once and retry. Don't refresh preemptively on every call — every unnecessary refresh rotates the refresh token, which can race with concurrent workers of your own.
---
## Sign in with Teachfloor (OpenID Connect)
If your app declares `oauth.authorize` in its manifest, users can sign in to your app via Teachfloor — same UX as "Sign in with Google" or "Sign in with GitHub." Adding the `openid` scope on the authorize request unlocks OpenID Connect: your callback receives an `id_token` (a signed JWT) identifying the user, in addition to the standard OAuth access + refresh tokens.
The primary use case: users installing your app via Teachfloor's marketplace get dropped onto your setup page already signed in — no re-authentication, no separate account creation.
### Discovery
Teachfloor publishes standard OIDC discovery so any conformant OIDC client library (Node `openid-client`, Python `authlib`, Go `go-oidc`, PHP `league/oauth2-client`, etc.) auto-configures with one URL:
```
https://app.teachfloor.com/.well-known/openid-configuration
```
Public keys for id_token signature verification:
```
https://app.teachfloor.com/.well-known/jwks.json
```
Both endpoints are cacheable (1h TTL); your OIDC client library handles caching automatically.
### The authorize URL
Redirect the user's browser to:
```
https://app.teachfloor.com/oauth/authorize
?client_id=
&response_type=code
&scope=openid+profile+email
&redirect_uri=
&state=
```
Required params:
- `client_id` — your app's client_id (from `teachfloor apps create` or the Developer dashboard)
- `response_type=code` — authorization-code flow
- `scope` — space-separated list; must include `openid` for OIDC; add `profile`/`email` for additional claims. Any scope not declared in your manifest's `permissions` returns `invalid_scope`
- `redirect_uri` — must match one of the `redirect_uris` declared in your manifest's `oauth.authorize` block exactly
- `state` — CSRF token you generate + verify on the callback
Optional:
- `prompt=none` — silent auth (see [below](#silent-sign-in))
- `organization=` — pin the flow to a specific organization when the user is a member of multiple orgs where your app is installed
### Handling the callback
Teachfloor redirects the user's browser to your `redirect_uri` with `?code=&state=`. Exchange the code for tokens:
```javascript
// Node example using stock fetch — most OIDC libraries do this for you
const response = await fetch('https://app.teachfloor.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authCode,
client_id: process.env.TEACHFLOOR_CLIENT_ID,
client_secret: process.env.TEACHFLOOR_CLIENT_SECRET,
redirect_uri: process.env.TEACHFLOOR_REDIRECT_URI,
}),
})
const tokens = await response.json()
// tokens contains: access_token, refresh_token, expires_in, token_type, scope, id_token
```
### Verifying the id_token
The `id_token` is a JWT signed with RS256. You MUST verify:
1. Signature — against Teachfloor's public key from the JWKS endpoint
2. `iss` claim equals `https://app.teachfloor.com`
3. `aud` claim equals your `client_id`
4. `exp` claim is in the future
Most OIDC client libraries do all four automatically. Example with Node's `openid-client`:
```javascript
import { Issuer } from 'openid-client'
// Once at startup
const teachfloor = await Issuer.discover('https://app.teachfloor.com')
const client = new teachfloor.Client({
client_id: process.env.TEACHFLOOR_CLIENT_ID,
client_secret: process.env.TEACHFLOOR_CLIENT_SECRET,
redirect_uris: [process.env.TEACHFLOOR_REDIRECT_URI],
response_types: ['code'],
})
// In your callback handler
const params = client.callbackParams(req)
const tokenSet = await client.callback(process.env.TEACHFLOOR_REDIRECT_URI, params, { state: savedState })
const claims = tokenSet.claims()
// claims: { sub, iss, aud, iat, exp, email, email_verified, name }
```
The `sub` claim is a stable identifier for the user across sessions — use it as your foreign-key when linking a Teachfloor user to a row in your database. `email` is convenient but users can change their email address; `sub` is guaranteed stable.
### Silent sign-in (`prompt=none`)
For zero-UI silent authentication — e.g. after a fresh install, dropping the user onto your setup page already signed in — add `&prompt=none` to the authorize URL. Behavior per OIDC Core §3.1.2.6:
- User is signed in to Teachfloor + your app is installed for their org → immediate redirect to your callback with `?code=...` (no consent screen shown)
- User is NOT signed in → redirect back with `?error=login_required` — fall back to your normal sign-in form
- Your app is not installed for any of the user's orgs → redirect back with `?error=interaction_required` — direct them to install first
- Requested scopes exceed what's in your manifest → redirect back with `?error=invalid_scope` — a real developer bug on your side
**Handling errors:** any `?error=...` on the callback is a normal failure mode, not an exception. Show a helpful message or redirect to a fallback (magic link, "install first" prompt, etc.) — don't crash.
### The `organization` disambiguation param
If a user belongs to multiple organizations that all have your app installed, the consent screen would normally show a picker. When you know which org the flow is for (e.g. post-install redirect from Teachfloor includes `?organization=`), forward that slug in your authorize URL:
```
https://app.teachfloor.com/oauth/authorize?...&organization=acme
```
Silent auth pins to that specific org. If the user isn't a member of that org (or the app isn't installed there), you get `interaction_required`.
### Recommended end-to-end pattern for post-install silent sign-in
1. In your manifest, `post_install_action.url` = `https://my-app.com/setup?organization=` (Teachfloor auto-appends the org slug)
2. On landing at your setup page, check if the user has an existing session on your side — if yes, render the setup UI
3. If no session, redirect the user's browser to `/oauth/authorize?...&prompt=none&organization=` (forward the org slug from the URL)
4. In your OIDC callback, exchange the code, verify the id_token, mint a session cookie keyed to the user's `sub` claim, redirect back to your setup page
5. On subsequent renders, the session cookie exists — go straight to the setup UI
Total user-visible latency for the silent auth: ~500ms-1s (comparable to "Sign in with Google" when you're already logged in).
### Failure fallback
Users who reach your setup page from a shared link or bookmark (no active Teachfloor session in this browser) will hit `error=login_required`. Provide a fallback flow — magic link, password login, or a "Sign in with Teachfloor" button that runs the same authorize URL WITHOUT `prompt=none` (which then shows the interactive consent screen).
## Revocation
**On uninstall** — every access token and refresh token issued for that installation is revoked automatically. The `app.uninstalled` webhook fires just before revocation; use it to purge the installation from your side:
```javascript
async function handleAppUninstalled(event) {
await deleteInstallation(event.data.organization.id)
}
```
After the uninstall completes, any API call with the old `access_token` returns 401 and any refresh with the old `refresh_token` returns `invalid_grant`. Neither can be recovered — the org would need to reinstall the app to get new tokens.
**On version upgrade** — the old tokens are revoked and the new version's install fires a fresh `app.installed` webhook with new tokens. If the new version's permissions changed, the new tokens carry the new scope set. Treat this the same as any other install event.
**On manifest re-push of the same version** — existing tokens keep working.
## Security
- **Never log or transmit `client_secret`, `access_token`, or `refresh_token` outside your controlled backend.** Redact them from error reports and observability tools.
- **Store tokens encrypted at rest.** If your database is compromised, plaintext tokens are as good as full account access.
- **Verify the webhook signature before trusting the payload.** The `credentials` block only appears in webhooks that pass HMAC-SHA256 signature verification with your app's signing secret — see [Signature verification](/docs/apps/advanced-topics/webhooks#signature-verification).
- **HTTPS only.** Every OAuth call is HTTPS-only. Any redirect URL you use for `post_install_action.url` or `oauth.authorize.redirect_uris` must be HTTPS (loopback `http://localhost` allowed for local dev only).
- **Scope-limit your tokens.** Only declare the permissions your app actually needs. A `courses:read` token can't reach `/v0/members` or any other endpoint outside its declared scopes.
**For `authorize` (OIDC) flows specifically:**
- **Always verify the id_token signature** against Teachfloor's JWKS. Never trust an id_token's claims without signature verification — use a standard OIDC client library that enforces this by default.
- **Always verify `iss`, `aud`, and `exp`** on incoming id_tokens. An attacker who obtained an id_token intended for a DIFFERENT app should not be able to sign in to yours.
- **Always validate the `state` parameter** on the callback. Reject callbacks whose `state` doesn't match what you generated when starting the flow. This is the CSRF defense for the OAuth code flow.
- **Never accept access tokens from the browser as evidence of identity.** Only the id_token, verified server-side, is trusted identity. Access tokens from the browser can be replayed or stolen.
- **Use short-lived session cookies keyed to `sub`.** Don't store the id_token in a cookie or in localStorage — turn it into your app's own session immediately, then discard.
## Next Steps
→ Continue to [Permissions](/docs/apps/advanced-topics/permissions)
## Additional Resources
- [Webhooks](/docs/apps/advanced-topics/webhooks) - The delivery mechanism for the `credentials` block on `app.installed`.
- [Permissions](/docs/apps/advanced-topics/permissions) - The manifest permission catalog; OAuth scopes are derived from a subset of it.
- [App Manifest](/docs/apps/core-concepts/app-manifest) - The full manifest schema, including the `oauth` block.
---
## Document: /docs/apps/advanced-topics/permissions
URL: /docs/apps/advanced-topics/permissions
# Permissions
Permissions control what your app can access on the Teachfloor platform.
## Overview
Permissions control three types of access:
1. **Contextual Data**: Data the platform includes in event callbacks (course, module, element)
2. **Storage & Features**: Platform features your app can use (data storage, AI generation)
3. **Public API**: Endpoints your app's servers can call via OAuth (`api.teachfloor.com/v0/*`) — see [OAuth](./oauth) for the full mapping.
All permissions must be declared in your app manifest with a user-facing explanation.
## How Permissions Work
Permissions are enforced on two surfaces:
- **SDK** — when your app subscribes to events, the platform includes an `objectContext` parameter containing contextual data based on your granted permissions. For example, with `courses:read` permission, `objectContext.course` includes the current course data when in a course viewport.
- **Public API** — when your app opts into OAuth, a subset of permissions is minted into the access token as OAuth scopes, unlocking the matching endpoints on `api.teachfloor.com/v0/*`. See [OAuth](./oauth) for details.
Some permissions apply to both surfaces (e.g. `courses:read` grants SDK context AND `GET /v0/courses/*`); others are surface-specific (e.g. `members:read` is API-only, `appdata:read` is SDK-only).
:::info
See [Integration Guide - Events](/docs/apps/core-concepts/extension-kit/integration#events) for detailed `objectContext` structure and usage examples.
:::
## Available Permissions
Your app can request the following permissions:
### Contextual Data Permissions
| Permission | Description | Access Type |
|-----------|-------------|-------------|
| `user:read` | Access user profile information | User data in objectContext |
| `user_events:read` | Access user activity events | User events |
| `courses:read` | Access course contextual data | Course object in objectContext |
| `modules:read` | Access module contextual data | Module object in objectContext |
| `elements:read` | Access element contextual data | Element object in objectContext |
### Storage Permissions
| Permission | Description | Access Type | Hierarchy |
|-----------|-------------|-------------|-----------|
| `appdata:read` | Read organization-wide app data | Storage API - App Data | Read only |
| `appdata:write` | Write organization-wide app data | Storage API - App Data | **Includes read** |
| `userdata:read` | Read user-specific app data | Storage API - User Data | Read only |
| `userdata:write` | Write user-specific app data | Storage API - User Data | **Includes read** |
| `usercollection:read` | Read user data collections | Storage API - Collections | Read only |
| `usercollection:write` | Write to user data collections | Storage API - Collections | **Includes read** |
:::info
**Important**: Write permissions (`*:write`) automatically grant read access. Requesting `*:write` is sufficient for both reading and writing.
:::
### AI & Feature Permissions
| Permission | Description | Access Type |
|-----------|-------------|-------------|
| `ai:text_generate` | Generate text using AI models | AI Generation API |
| `ai:context_external_send` | Send platform data to AI models | AI Context Sharing |
### Realtime Permissions
| Permission | Description | Access Type |
|-----------|-------------|-------------|
| `realtime` | Publish and subscribe to the app's realtime channels | Realtime SDK |
### Public API Permissions
These permissions grant your app's servers access to the Teachfloor public API via the OAuth token. They are **not** available through the SDK — only your backend can use them by calling `https://api.teachfloor.com/v0/*` with the app's access token.
| Permission | Description | Access Type |
|-----------|-------------|-------------|
| `members:read` | Read organization members and their course enrollments | Public API only |
| `activities:read` | Read activity records generated by members | Public API only |
:::info
Note that `courses:read`, `modules:read`, and `elements:read` also unlock the matching public-API endpoints (`GET /v0/courses/*`, etc.) when your app opts into OAuth. See [OAuth](./oauth) for the complete manifest-permission → OAuth-scope mapping.
:::
## Permission Details
### User Permissions
#### `user:read`
Access basic user profile information.
**Data available in objectContext**:
- User ID
- Full name
- Email address
- Avatar URL
- Language preference
- Timezone
**Use cases**:
- Personalization
- User greetings
- Profile displays
**Example**:
```json
{
"permission": "user:read",
"purpose": "Display personalized greetings and user information"
}
```
#### `user_events:read`
Access user activity and learning events.
**Data available in objectContext**:
- Course enrollments
- Module completions
- Element interactions
- Login history
- Activity timestamps
**Use cases**:
- Progress tracking
- Analytics dashboards
- Activity feeds
- Engagement metrics
**Example**:
```json
{
"permission": "user_events:read",
"purpose": "Track learning progress and generate activity reports"
}
```
### Contextual Permissions
These permissions control what data appears in the `objectContext` parameter based on the current viewport.
#### `courses:read`
Access course information when user is in a course viewport.
**Data available in objectContext.course**:
- Course ID
- Course title
- Description
- Status
- Enrollment data
- Course settings
**Available in viewports**:
- `teachfloor.dashboard.course.detail`
- `teachfloor.dashboard.course.module.detail`
- `teachfloor.dashboard.course.element.detail`
**Example**:
```json
{
"permission": "courses:read",
"purpose": "Display course information in notes and widgets"
}
```
**Usage**:
```javascript
import { subscribeToEvent } from '@teachfloor/extension-kit'
subscribeToEvent('environment.viewport.changed', (viewport, objectContext) => {
if (objectContext.course) {
console.log('Course title:', objectContext.course.name)
console.log('Course ID:', objectContext.course.id)
}
})
```
#### `modules:read`
Access module information when user is viewing a module.
**Data available in objectContext.module**:
- Module ID
- Module title
- Description
- Order/sequence
- Completion status
**Available in viewports**:
- `teachfloor.dashboard.course.module.detail`
- `teachfloor.dashboard.course.element.detail`
**Example**:
```json
{
"permission": "modules:read",
"purpose": "Show module progress and navigation"
}
```
**Usage**:
```javascript
import { subscribeToEvent } from '@teachfloor/extension-kit'
subscribeToEvent('environment.viewport.changed', (viewport, objectContext) => {
if (objectContext.module) {
console.log('Module title:', objectContext.module.name)
console.log('Module order:', objectContext.module.position)
}
})
```
#### `elements:read`
Access learning element information when user is viewing an element.
**Data available in objectContext.element**:
- Element ID
- Element type (video, assignment, quiz, etc.)
- Title and description
- Content metadata
- Completion status
**Available in viewports**:
- `teachfloor.dashboard.course.element.detail`
**Example**:
```json
{
"permission": "elements:read",
"purpose": "Display element information and add notes to content"
}
```
**Usage**:
```javascript
import { subscribeToEvent } from '@teachfloor/extension-kit'
subscribeToEvent('environment.viewport.changed', (viewport, objectContext) => {
if (objectContext.element) {
console.log('Element type:', objectContext.element.type)
console.log('Element title:', objectContext.element.name)
console.log('Completed:', objectContext.element.completed)
}
})
```
### Storage Permissions
Storage permissions allow your app to persist data on the Teachfloor platform. See [Data Storage](./data-storage) for detailed usage.
#### `appdata:read` & `appdata:write`
Store and retrieve organization-wide app data shared across all users.
:::info
**Permission Hierarchy**: `appdata:write` includes `appdata:read` access.
:::
**Use cases**:
- App configuration
- Global settings
- Shared templates
- Feature flags
**Example (Read and Write)**:
```json
{
"permissions": [
{
"permission": "appdata:write",
"purpose": "Save and load app configuration and shared settings"
}
]
}
```
**Example (Read-Only)**:
```json
{
"permissions": [
{
"permission": "appdata:read",
"purpose": "Load app configuration and shared settings"
}
]
}
```
**Usage**:
```javascript
import { store, retrieve } from '@teachfloor/extension-kit'
// Write app data
await store('config', { theme: 'dark', lang: 'en' }, 'appdata')
// Read app data
const config = await retrieve('config', 'appdata')
```
#### `userdata:read` & `userdata:write`
Store and retrieve user-specific data.
**Permission Hierarchy**: `userdata:write` includes `userdata:read` access.
**Use cases**:
- User preferences
- Personal settings
- User state
- Draft content
**Example (Read and Write)**:
```json
{
"permissions": [
{
"permission": "userdata:write",
"purpose": "Save and load your personal preferences and settings"
}
]
}
```
**Example (Read-Only)**:
```json
{
"permissions": [
{
"permission": "userdata:read",
"purpose": "Load your personal preferences and settings"
}
]
}
```
**Usage**:
```javascript
import { store, retrieve } from '@teachfloor/extension-kit'
// Write user data
await store('preferences', { theme: 'light', fontSize: 14 }, 'userdata')
// Read user data
const prefs = await retrieve('preferences', 'userdata')
```
#### `usercollection:read` & `usercollection:write`
Store and retrieve collections of data items for a user, with pagination support.
:::info
**Permission Hierarchy**: `usercollection:write` includes `usercollection:read` access.
:::
**Use cases**:
- Activity logs
- User notes or annotations
- Saved items
- History data
**Example (Read and Write)**:
```json
{
"permissions": [
{
"permission": "usercollection:write",
"purpose": "Save and load your notes and activity history"
}
]
}
```
**Example (Read-Only)**:
```json
{
"permissions": [
{
"permission": "usercollection:read",
"purpose": "Load your saved notes and activity history"
}
]
}
```
**Usage**:
```javascript
import { store, retrieve } from '@teachfloor/extension-kit'
// Add item to collection
await store('notes', {
title: 'My Note',
content: 'Content...',
createdAt: Date.now()
}, 'usercollection')
// Retrieve collection with pagination
const result = await retrieve('notes?limit=10', 'usercollection')
console.log(result.data) // Array of items
console.log(result.next) // Next cursor
```
### AI Permissions
#### `ai:text_generate`
Generate text using AI language models.
**Use cases**:
- Content generation
- Text completion
- Summarization
- Translation
**Example**:
```json
{
"permission": "ai:text_generate",
"purpose": "Generate content suggestions and summaries"
}
```
**Usage**:
```javascript
import { generate } from '@teachfloor/extension-kit'
// Generate text using AI
const result = await generate(
'Write a summary of this course',
'ai/text-generate'
)
console.log(result) // Generated text response
```
#### `ai:context_external_send`
Permission to use platform data placeholders in AI prompts.
**How it works**: This permission is **only checked when you use placeholders** like `{{course.name}}` or `{{module.content}}` in your AI prompts. Without this permission, you can still use `generate()` with regular prompts.
**Supported Placeholders**:
- `{{course.name}}` - Course title
- `{{course.content}}` - Course content (text format)
- `{{module.name}}` - Module title
- `{{module.content}}` - Module content (text format)
- `{{element.name}}` - Element title
- `{{element.content}}` - Element content (text format)
:::caution
**Important**: When using placeholders, you must also have the corresponding read permission:
- Course placeholders require `courses:read`
- Module placeholders require `modules:read`
- Element placeholders require `elements:read`
:::
**Example**:
```json
{
"permissions": [
{
"permission": "ai:text_generate",
"purpose": "Generate content suggestions"
},
{
"permission": "courses:read",
"purpose": "Access course information"
},
{
"permission": "ai:context_external_send",
"purpose": "Include course content in AI prompts"
}
]
}
```
**Usage**:
```javascript
import { generate } from '@teachfloor/extension-kit'
// Without placeholders - only needs ai:text_generate
const simpleResult = await generate('Write a motivational message')
// With placeholders - needs ai:text_generate + ai:context_external_send + courses:read
const contextResult = await generate(
'Summarize this course: {{course.content}}'
)
// Multiple placeholders
const detailedResult = await generate(
'Create a quiz about {{module.name}} covering: {{module.content}}'
)
```
### Realtime Permission
#### `realtime`
Publish and subscribe to your app's realtime channels. Enables sub-second event delivery between an app's SDK views (widgets, drawers, pages) while learners are active.
**Use cases**:
- Live collaboration UIs (cursors, presence, live-edit)
- Broadcasting state changes from one widget instance to others
- Instructor-side dashboards that react to learner activity in real time
**Example**:
```json
{
"permission": "realtime",
"purpose": "Broadcast live collaboration cursors between learners viewing this widget"
}
```
**Usage**:
```javascript
import { realtime, useExtensionContext } from '@teachfloor/extension-kit'
const courseId = useExtensionContext().environment.context.course?.id
// Subscribe to a course-scoped channel and receive events other
// clients publish on it.
const channel = realtime.subscribe({ scope: 'course', id: courseId })
channel.on('cursor.moved', (message) => renderRemoteCursor(message.data))
// Broadcast to every other subscriber on the same channel.
channel.publish('cursor.moved', { x: 120, y: 340 })
```
Subscribing to a `course`-scoped channel additionally needs `courses:read` (and the same pattern for `modules:read` / `elements:read`) — without it, the host strips the resource id from the viewport payload and there's no id to subscribe with. See [Realtime Channels](./realtime) for the full channel model, event shape, and delivery guarantees.
### Public API Permissions
These permissions apply only to server-to-server calls via the app's OAuth token — they don't expose data through the SDK. Your app needs to opt into OAuth (see [OAuth](./oauth)) for them to have any effect.
#### `members:read`
Read organization members and their course enrollments via the public API.
**Grants access to**:
- `GET /v0/members` — list all members
- `GET /v0/members/search` — search members
- `GET /v0/members/{id}` — fetch a specific member
- `GET /v0/members/{id}/courses` — list a member's course enrollments (requires `courses:read`)
- `GET /v0/courses/{id}/members` — list members enrolled in a course (requires `courses:read`)
- `GET /v0/courses/{id}/members/{member_id}` — fetch a specific enrollment (requires `courses:read`)
**Use cases**:
- Sync members into an external CRM or HR system
- Reporting on enrollment across your customer base
**Example**:
```json
{
"permission": "members:read",
"purpose": "Sync course enrollments to our HR system"
}
```
#### `activities:read`
Read activity records generated by members interacting with course elements.
**Grants access to**:
- `GET /v0/activities` — list all activities
- `GET /v0/activities/{id}` — fetch a specific activity
- `GET /v0/elements/{id}/activities` — list activities for a given element (requires `elements:read`)
**Use cases**:
- Feed learner progress into an external analytics dashboard
- Trigger downstream automations when specific activity types occur
**Example**:
```json
{
"permission": "activities:read",
"purpose": "Export completion data to our LMS reporting tool"
}
```
## Permission Management
### Adding Permissions
#### Using CLI
```bash
teachfloor apps grant permission
```
Select permission and enter purpose when prompted.
#### Manual Addition
Edit `teachfloor-app.json`:
```json
{
"permissions": [
{
"permission": "courses:read",
"purpose": "Display course information in widgets"
},
{
"permission": "user_events:read",
"purpose": "Track your learning progress"
}
]
}
```
### Removing Permissions
#### Using CLI
```bash
teachfloor apps revoke permission
```
#### Manual Removal
Remove from manifest:
```json
{
"permissions": [
// Remove the permission object you no longer need
]
}
```
### Permission Purposes
Each permission must have a clear, user-facing explanation.
**Good purposes**:
- "Display course information in your notes" ✓
- "Show your current module progress" ✓
- "Track your learning progress for analytics" ✓
**Poor purposes**:
- "Access data" ✗ (too vague)
- "Platform integration" ✗ (not user-facing)
- "Required for functionality" ✗ (not specific)
## Using Permissions
Always check if contextual data exists before accessing it:
```javascript
import { subscribeToEvent } from '@teachfloor/extension-kit'
subscribeToEvent('environment.viewport.changed', (viewport, objectContext) => {
// Check before accessing
if (objectContext.course) {
console.log('Course:', objectContext.course.name)
}
// Use optional chaining
const moduleTitle = objectContext.module?.title || 'No module'
})
```
:::info
See [Integration Guide - Events](/docs/apps/core-concepts/extension-kit/integration#events) for complete `objectContext` usage examples.
:::
## Next Steps
→ Continue to [Deployment](./deployment)
## Additional Resources
- [Integration Guide](/docs/apps/core-concepts/extension-kit/integration) - Using permissions with events and storage
- [Best Practices](/docs/apps/references/best-practices) - Permission best practices and patterns
- [Examples](/docs/apps/references/examples) - Complete permission usage examples
---
## Document: /docs/apps/advanced-topics/deployment
URL: /docs/apps/advanced-topics/deployment
# Deployment
This guide covers deploying your Teachfloor app from private testing to public marketplace publication.
## Overview
Teachfloor apps can be deployed in two ways:
1. **Private Deployment**: Available only to your organization
2. **Public Deployment**: Listed in the Teachfloor Marketplace for all organizations
## Distribution Types
### Private Apps
Default distribution type for new apps.
**Characteristics**:
- Only visible to your organization
- No review process required
- Instant deployment
- Cannot be installed by other organizations
**When to use**:
- Internal productivity tools
- Organization-specific integrations
- Testing and development
- Custom solutions for your team
### Public Apps
Marketplace-listed apps available to all organizations.
**Characteristics**:
- Listed in Teachfloor Marketplace
- Requires review and approval
- Available to all organizations
- Subject to quality standards
**When to use**:
- General-purpose tools
- Integrations with popular services
- Apps that benefit the community
- Commercial offerings
## Deployment Process
### Step 1: Prepare Your App
#### Update Version
Increment the version in your manifest before each deployment:
```json
{
"version": "1.0.1" // Changed from "1.0.0"
}
```
Follow [semantic versioning](https://semver.org/):
- **Major** (x.0.0): Breaking changes
- **Minor** (1.x.0): New features, backward compatible
- **Patch** (1.0.x): Bug fixes
#### Test Locally
```bash
teachfloor apps start
```
Ensure:
- All features work as expected
- No console errors
- All viewports display correctly
- Data storage works properly
- Permissions are correctly requested
#### Clean Build
```bash
# Clear cache
rm -rf node_modules/.cache
rm -rf dist/
# Fresh install
npm install
# Test build
npm run build
```
### Step 2: Set Distribution Type
#### For Private Deployment
Apps are private by default. To explicitly set:
```bash
teachfloor apps set distribution
```
Select `private` when prompted.
**Or edit manifest manually**:
```json
{
"distribution_type": "private"
}
```
#### For Public Deployment
**Required** before submitting to marketplace:
```bash
teachfloor apps set distribution
```
Select `public` when prompted.
**Or edit manifest manually**:
```json
{
"distribution_type": "public"
}
```
:::caution
This must be set to `public` before the app can be submitted for marketplace review.
:::
### Step 3: Upload Your App
Build and upload your app:
```bash
teachfloor apps upload
```
**What happens**:
1. Validates manifest
2. Checks version isn't already approved
3. Runs `npm run build`
4. Bundles all files from `dist/`
5. Replaces localhost URLs with production URLs
6. Uploads to Teachfloor platform
7. Creates new app version
**Output**:
```
✓ Uploading manifest...
✓ Starting upload...
✓ Building the production bundle...
✓ Uploading files...
✓ App uploaded successfully.
```
### Step 4: Submit for Review (Public Apps Only)
#### Via Dashboard
1. Log in to Teachfloor dashboard
2. Navigate to **Settings → Apps**
3. Find your app in the list
4. Click **"Submit for Review"**
5. Fill out submission form (if any)
6. Click **"Submit"**
#### What Gets Reviewed
- **Functionality**: App works as described
- **UI/UX**: Follows Teachfloor design guidelines
- **Security**: No vulnerabilities or malicious code
- **Privacy**: Complies with data protection standards
- **Permissions**: Requests only necessary permissions
- **Documentation**: Clear purpose and explanation
### Step 5: Review Process
#### Timeline
- **Initial Review**: 2-5 business days
- **Revisions**: 1-3 business days per iteration
- **Final Approval**: 1 business day
#### Status Tracking
Check status in dashboard:
- `UNPUBLISHED`: Draft, not submitted
- `SUBMITTED`: Awaiting review
- `REVIEWING`: Under review
- `PUBLISHED`: Approved and live
- `REJECTED`: Review failed
#### If Rejected
You'll receive feedback explaining:
- Issues found
- Required changes
- Guidelines violated
**Fix and resubmit**:
1. Make required changes
2. Update version number
3. Run `teachfloor apps upload`
4. Submit again via dashboard
## Version Management
### Creating New Versions
Once a version is approved/published, it becomes **locked** and cannot be modified.
**To release updates**:
1. Update version in manifest:
```json
{
"version": "1.1.0" // Increment from 1.0.0
}
```
2. Make your changes
3. Upload:
```bash
teachfloor apps upload
```
4. Submit for review (if public)
### Version States
| State | Editable | Installable | Visible in Marketplace |
|-------|----------|-------------|------------------------|
| UNPUBLISHED | ✅ Yes | ✅ Yes (dev mode) | ❌ No |
| SUBMITTED | ❌ No | ✅ Yes (dev mode) | ❌ No |
| REVIEWING | ❌ No | ✅ Yes (dev mode) | ❌ No |
| PUBLISHED | ❌ No | ✅ Yes | ✅ Yes (if public) |
| REJECTED | ✅ Yes | ✅ Yes (dev mode) | ❌ No |
### Rollback Strategy
You cannot rollback a published version. To address issues:
- Publish a fixed version with incremented version number
- Contact support to hide from marketplace if needed
## Private to Public Migration
If you want to make an existing private app public:
1. Set distribution to public:
```bash
teachfloor apps set distribution
# Select: public
```
2. Update manifest:
```json
{
"distribution_type": "public",
"description": "Clear, helpful description for marketplace"
}
```
3. Increment version (use major version for public release)
4. Upload: `teachfloor apps upload`
5. Submit via dashboard
:::info
Existing private installations remain unaffected.
:::
## Marketplace Guidelines
### App Requirements
**Required**:
- Clear, descriptive name
- App icon
- Accurate description (50-200 characters)
- Valid semantic version
- Working functionality
**Recommended**:
- Settings page for configuration
- Post-install action
- Clear permission purposes
### Quality Standards
Public apps must meet quality standards for performance, accessibility, design, error handling, and privacy.
## Next Steps
→ Continue to [CLI Reference](/docs/apps/references/cli)
## Additional Resources
- [Best Practices](/docs/apps/references/best-practices) - Deployment best practices and checklists
- [Troubleshooting Guide](/docs/apps/references/troubleshooting) - Common deployment issues
- [App Manifest](/docs/apps/core-concepts/app-manifest) - Manifest configuration
---
## Document: /docs/apps/references/cli
URL: /docs/apps/references/cli
# CLI Reference
Complete reference for Teachfloor CLI commands and options.
## Installation
```bash
npm install -g @teachfloor/teachfloor-cli
```
## Command Reference
### Quick Reference Table
| Command | Description | Requires Auth | Requires App Folder |
|---------|-------------|---------------|---------------------|
| **Global** |
| `teachfloor version` | Display CLI version | No | No |
| `teachfloor login` | Authenticate with Teachfloor | No | No |
| `teachfloor logout` | Log out from account | No | No |
| `teachfloor whoami` | Show current user and org | Yes | No |
| **App Management** |
| `teachfloor apps create ` | Create new app | Yes | No |
| `teachfloor apps start` | Start dev server | Yes | Yes |
| `teachfloor apps upload` | Build and upload app | Yes | Yes |
| **View Management** |
| `teachfloor apps add view` | Add view to app | Yes | Yes |
| `teachfloor apps remove view` | Remove view from app | Yes | Yes |
| `teachfloor apps add settings` | Add settings view | Yes | Yes |
| `teachfloor apps add widget` | Add widget to app | Yes | Yes |
| `teachfloor apps remove widget` | Remove widget from app | Yes | Yes |
| **Permission Management** |
| `teachfloor apps grant permission` | Add permission | Yes | Yes |
| `teachfloor apps revoke permission` | Remove permission | Yes | Yes |
| **Webhook & OAuth** |
| `teachfloor apps set webhook` | Configure webhook URL and events | Yes | Yes |
| `teachfloor apps remove webhook` | Remove webhook block | Yes | Yes |
| `teachfloor apps set oauth` | Configure OAuth grant type | Yes | Yes |
| `teachfloor apps remove oauth` | Remove OAuth block | Yes | Yes |
| **Distribution** |
| `teachfloor apps set distribution` | Set public/private | Yes | Yes |
| **Inspection** |
| `teachfloor apps show` | Print a spec-sheet summary of the current app | Yes | Yes |
| **Diagnostics** |
| `teachfloor apps doctor` | Diagnose common setup issues | Yes | No |
## Non-Interactive Mode
Every prompt in every command has a matching flag. Pass all the flags a command needs and the CLI runs end-to-end without asking questions — required for scripts, CI pipelines, and AI-driven workflows.
**Three triggers** (any one flips the CLI into non-interactive mode):
- `--nonInteractive` on the command (alias: `--non-interactive`, `--no-interactive`).
- Environment variable: `TF_NON_INTERACTIVE=1` or `CI=1`.
- Automatic: whenever stdin isn't a TTY (piped input, subprocess spawn, headless agent).
**In non-interactive mode**:
- A prompt whose flag is set → uses the flag value (validated).
- A prompt whose flag is missing but has a default → uses the default (validated).
- A prompt whose flag is missing and has no default → hard error: `Missing required input in non-interactive mode: pass -- to set "".`
- Invalid values (bad choice, failed regex, empty required string) → error naming the flag, with the underlying validator's message.
**Example** — scripted `apps create`:
```bash
teachfloor apps create my-app \
--name "My App" \
--description "A test app" \
--version 1.0.0
```
No `--nonInteractive` needed when stdin isn't a TTY — piping / subprocess spawn triggers it automatically. Add the flag explicitly in wrapper scripts if you want fail-fast behavior regardless of shell context.
**Flag → prompt mapping** (per command):
| Command | Flags |
|---|---|
| `apps create ` | `--appId`, `--name`, `--description`, `--version` |
| `apps add view` | `--viewport`, `--componentName`, `--withExample`, `--overwrite` |
| `apps add settings` | `--componentName`, `--withExample` |
| `apps add widget` | `--viewport`, `--id`, `--name`, `--description`, `--componentName`, `--withExample`, `--overwrite` |
| `apps remove view` | `--viewport`, `--removeComponent` |
| `apps remove widget` | `--id`, `--removeComponent` |
| `apps grant permission` | `--permissionName`, `--explanation` |
| `apps revoke permission` | `--permissionName` |
| `apps set webhook` | `--url`, `--events` (repeatable, or comma-separated) |
| `apps set oauth` | `--oauthType` |
| `apps set distribution` | `--distributionType` |
`login` remains interactive-only (browser OAuth).
## Global Commands
### `teachfloor version`
Display CLI version and check for updates.
```bash
teachfloor version
```
**Output**:
```
teachfloor version 1.2.0
A newer version of the Teachfloor CLI is available: 1.3.0
```
---
### `teachfloor login`
Authenticate with your Teachfloor account.
```bash
teachfloor login
```
**Prompts**:
- Email address
- Password
- Organization (if you have multiple)
**What it does**:
1. Authenticates with your Teachfloor account
2. Stores credentials securely
3. Saves selected organization
**Example**:
```bash
$ teachfloor login
✔ Enter your email: john@example.com
✔ Enter your password: ••••••••
✔ Select an organization: My Organization
✓ Login successful!
```
---
### `teachfloor logout`
Log out from your Teachfloor account.
```bash
teachfloor logout
```
**What it does**:
1. Removes stored credentials
2. Clears organization selection
---
### `teachfloor whoami`
Display current authenticated user and organization.
```bash
teachfloor whoami
```
**Output**:
```
User: john@example.com
Organization: my-organization
```
---
## App Management
### `teachfloor apps create `
Create a new Teachfloor app.
```bash
teachfloor apps create my-awesome-app
```
**Arguments**:
- `app-name`: Name of the folder to create
**Prompts** (interactive mode) / **Flags** (non-interactive):
- **App ID** — `--appId ` (alias `--id`; default: `-`)
- **Display Name** — `--name ` (required)
- **Description** — `--description ` (required)
- **Version** — `--version ` (semver, default: `1.0.0`)
**What it does**:
1. Creates app on the platform
2. Generates project structure with all necessary files
3. Installs dependencies
**Generated Structure**:
```
my-awesome-app/
├── src/
│ ├── index.js
│ └── views/
│ └── App.jsx
├── public/
│ └── index.html
├── teachfloor-app.json
├── package.json
├── webpack.config.js
└── tsconfig.json
```
**Example**:
```bash
$ teachfloor apps create notes-app
✔ App ID: notes-1234567890
✔ Display Name: Notes App
✔ Description: Take notes while learning
✔ Version: 1.0.0
✓ Creating app...
✓ Setting up app structure...
✓ Installing npm dependencies...
✓ App "Notes App" created successfully in "notes-app".
OAuth credentials for this app:
Client ID: 9f8e7d6c-1234-4abc-9def-0123456789ab
Client Secret: rN7pQ8E4fD0sUjLvX2mK5H1a3bT9wY6c
```
:::caution
**Save the Client Secret now.** It's shown once at create time and never returned by the API again — if you lose it, you can retrieve it from Developers → Apps → your app → **OAuth Client Secret** in the Teachfloor dashboard. The Client ID is also delivered inside every `app.installed` webhook payload; the Client Secret is not. See [OAuth](./oauth) for how to use these credentials for the refresh flow.
:::
---
### `teachfloor apps start`
Start development server for your app.
```bash
teachfloor apps start
```
**Options**:
- `-m, --manifest `: Use custom manifest file
**What it does**:
1. Validates your app manifest
2. Uploads manifest to platform
3. Opens browser to install the app
4. Starts development server with auto-reload
**Requirements**:
- Must be run inside an app folder
- Must be logged in
- Version must not be approved/published
**Example**:
```bash
$ cd my-app
$ teachfloor apps start
✓ Manifest file updated
Install URL: https://app.teachfloor.com/myorg/courses?app=abc123@1.0.0
Starting development server...
webpack 5.x.x compiled successfully
```
**With custom manifest**:
```bash
teachfloor apps start --manifest teachfloor-app.dev.json
```
---
### `teachfloor apps upload`
Build and upload your app to the platform.
```bash
teachfloor apps upload
```
**What it does**:
1. Builds your app for production
2. Uploads bundled files to the platform
3. Creates a new version
**Requirements**:
- Must be run inside an app folder
- Must be logged in
- Version must not be already published
**Example**:
```bash
$ teachfloor apps upload
✓ Building the production bundle...
✓ Uploading files...
✓ App uploaded successfully.
```
---
## View Management
### `teachfloor apps add view`
Add a new view to your app.
```bash
teachfloor apps add view
```
**Prompts** (interactive mode) / **Flags** (non-interactive):
- **Select viewport** — `--viewport ` (must be one of the app's available viewports)
- **Component name** — `--componentName ` (alias `--component`; defaults to a name derived from the viewport)
- **Generate example** — `--withExample` (alias `--with-example`; default: `false`)
- **Overwrite existing file** — `--overwrite` (only prompted when the target file exists; default: `false`)
**What it does**:
1. Shows available viewports for your app
2. Creates component file in `src/views/`
3. Updates your app manifest
**Example**:
```bash
$ teachfloor apps add view
✔ Select the viewport for your view: teachfloor.dashboard.course.list
✔ Enter the name of your component: CourseListView
✔ Generate a "Getting Started" example view? Yes
✓ Component view created at src/views/CourseListView.jsx
✓ Manifest file updated
✓ View "CourseListView" added successfully under "teachfloor.dashboard.course.list".
```
**Generated Component**:
```jsx
import React from 'react'
import { Container, Text } from '@teachfloor/extension-kit'
const CourseListView = () => {
return (
CourseListView
)
}
export default CourseListView
```
---
### `teachfloor apps remove view`
Remove a view from your app.
```bash
teachfloor apps remove view
```
**Prompts** (interactive mode) / **Flags** (non-interactive):
- **Select viewport** — `--viewport ` (must match an existing view in the manifest)
- **Delete component file too** — `--removeComponent` (alias `--remove-component`; default: `false`)
**What it does**:
1. Removes view from your app manifest
2. Note: Component file remains in `src/views/` (delete manually if needed)
**Example**:
```bash
$ teachfloor apps remove view
✔ Select the viewport to remove: teachfloor.dashboard.course.list
✓ View removed successfully.
```
---
### `teachfloor apps add settings`
Add a settings view to your app.
```bash
teachfloor apps add settings
```
**Prompts** (interactive mode) / **Flags** (non-interactive):
- **Component name** — `--componentName ` (alias `--component`; default: `AppSettings`)
- **Generate example** — `--withExample` (alias `--with-example`; default: `false`)
**What it does**:
1. Creates settings component in `src/views/`
2. Adds `settings` viewport to your manifest
**Example**:
```bash
$ teachfloor apps add settings
✔ Enter the name of your component: AppSettings
✔ Generate a "Getting Started" example view? Yes
✓ Settings view created at src/views/AppSettings.jsx
✓ Manifest file updated
```
---
### `teachfloor apps add widget`
Add a new widget to your app. See [Surfaces](./surfaces) for the concepts (widget id, ``, admin picker).
```bash
teachfloor apps add widget
```
**Prompts** (interactive mode) / **Flags** (non-interactive):
- **Select viewport** — `--viewport ` (`"*"` for universal, or a concrete widget-hosting viewport)
- **Widget id** — `--id ` (lowercase slug, `^[a-z][a-z0-9_]*$`, unique per app across all widget declarations)
- **Widget name** — `--name ` (≤60 chars; shown in the admin's widget picker and the app install-consent surfaces list)
- **Widget description** — `--description ` (≤200 chars; shown alongside the name in the picker)
- **Component name** — `--componentName ` (alias `--component`; defaults to `Widget` derived from the widget id — e.g. `streak_daily` → `StreakDailyWidget`)
- **Generate example** — `--withExample` (alias `--with-example`; default: `false`)
- **Overwrite existing file** — `--overwrite` (only prompted when the target file exists; default: `false`)
**What it does**:
1. Prompts for the widget's scoping viewport
2. Validates the widget id locally against ids already in your manifest (fails fast before the server round-trip)
3. Creates the component file in `src/views/`
4. Appends a `surface: "widget"` view entry to your manifest with the nested `widget: { id, name, description }` block
**Example**:
```bash
$ teachfloor apps add widget
✔ Select the viewport for your widget: *
✔ Enter the widget id (lowercase slug): learning_streak
✔ Enter the widget name: Learning Streak
✔ Enter the widget description: Current daily study streak with a 7-day heatmap.
✔ Enter the name of your component: LearningStreakWidget
✔ Generate a "Getting Started" example widget? Yes
✓ Component view created at src/views/LearningStreakWidget.jsx
✓ Manifest file updated
✓ Widget "Learning Streak" added successfully under "*".
```
**Generated Manifest Entry**:
```json
{
"surface": "widget",
"viewport": "*",
"component": "LearningStreakWidget",
"widget": {
"id": "learning_streak",
"name": "Learning Streak",
"description": "Current daily study streak with a 7-day heatmap."
}
}
```
---
### `teachfloor apps remove widget`
Remove a widget from your app.
```bash
teachfloor apps remove widget
```
**Prompts** (interactive mode) / **Flags** (non-interactive):
- **Select widget** — `--id ` (must match a widget id declared in your manifest; picker lists each widget as ` — ()`)
- **Delete component file too** — `--removeComponent` (alias `--remove-component`; default: `false`)
**What it does**:
1. Removes the widget's view entry from your manifest (matched by `widget.id`, not viewport — multiple widgets can share the same viewport)
2. Optionally deletes the component file in `src/views/` when `--removeComponent` is set
**Example**:
```bash
$ teachfloor apps remove widget
✔ Select the widget you want to remove: learning_streak — Learning Streak (*)
✔ Do you want to delete the component file as well? Yes
✓ Component file "src/views/LearningStreakWidget.jsx" deleted
✓ Manifest file updated
✓ Widget "Learning Streak" removed successfully.
```
---
## Permission Management
### `teachfloor apps grant permission`
Add a permission to your app.
```bash
teachfloor apps grant permission
```
**Prompts** (interactive mode) / **Flags** (non-interactive):
- **Select permission** — `--permissionName ` (alias `--permission`; must be one of the available permissions and not already granted)
- **Purpose** — `--explanation ` (required — user-facing reason shown on the install screen)
**Available Permissions**:
Contextual data (SDK):
- `user:read`: Read user profile
- `user_events:read`: Read user activity
- `courses:read`: Read course data (also unlocks `GET /v0/courses/*` when OAuth is opted in)
- `modules:read`: Read module content (also unlocks `GET /v0/modules/*` when OAuth is opted in)
- `elements:read`: Read learning elements (also unlocks `GET /v0/elements/*` when OAuth is opted in)
Data storage (SDK):
- `appdata:read` / `appdata:write`: Organization-wide app storage
- `userdata:read` / `userdata:write`: User-specific storage
- `usercollection:read` / `usercollection:write`: User-specific collection storage
AI (SDK):
- `ai:text_generate`: Consume AI credits to generate text
- `ai:context_external_send`: Include platform data in external AI requests
Realtime (SDK):
- `realtime`: Publish and subscribe to the app's realtime channels
Public API only (backend/OAuth):
- `members:read`: Read organization members and enrollments via `GET /v0/members/*`
- `activities:read`: Read activity records via `GET /v0/activities/*`
See [Permissions](/docs/apps/advanced-topics/permissions) for the full description of each permission and [OAuth](/docs/apps/advanced-topics/oauth) for the scope mapping.
**Example**:
```bash
$ teachfloor apps grant permission
✔ Select permission: courses:read
✔ Enter purpose: Display course information in notes
✓ Permission added to manifest.
```
**Updates Manifest**:
```json
{
"permissions": [
{
"permission": "courses:read",
"purpose": "Display course information in notes"
}
]
}
```
---
### `teachfloor apps revoke permission`
Remove a permission from your app.
```bash
teachfloor apps revoke permission
```
**Prompts** (interactive mode) / **Flags** (non-interactive):
- **Select permission** — `--permissionName ` (alias `--permission`; must match an existing granted permission)
**Example**:
```bash
$ teachfloor apps revoke permission
✔ Select permission to revoke: courses:read
✓ Permission removed from manifest.
```
---
## Webhook & OAuth
### `teachfloor apps set webhook`
Configure the app's webhook URL and the events it subscribes to. Re-run to reconfigure — the whole block is rewritten each time.
```bash
teachfloor apps set webhook
```
**Prompts** (interactive mode) / **Flags** (non-interactive):
- **URL** — `--url ` (required — must start with `https://`, max 2048 chars)
- **Events** — `--events ` (multi-select checkbox picker; repeatable flag or comma-separated: `--events a --events b` OR `--events "a,b"`)
**Available Events**:
Populated from the server catalog at run time so the CLI always offers exactly the events the platform will accept on upload. Current set includes:
- `organization.join`, `course.created`, `course.updated`, `course.completed`, `course.join`
- `module.created`, `module.updated`
- `element.created`, `element.updated`, `element.deleted`, `element.completed`
- `member.login`
The lifecycle events `app.installed` and `app.uninstalled` are always delivered — no need to include them in the manifest.
**Example**:
```bash
$ teachfloor apps set webhook
✔ Webhook URL (must be https://): https://myapp.example.com/teachfloor/hook
✔ Select events to subscribe to (space to toggle): course.completed, element.completed
✓ Webhook set to "https://myapp.example.com/teachfloor/hook" (2 events).
```
**Updates Manifest**:
```json
{
"webhook": {
"url": "https://myapp.example.com/teachfloor/hook",
"events": ["course.completed", "element.completed"]
}
}
```
---
### `teachfloor apps remove webhook`
Strip the `webhook` block from the manifest. The app becomes SDK-only for platform events — no signed deliveries, no installer identity disclosure to the app, and no OAuth credentials on install (OAuth requires a webhook to deliver them).
```bash
teachfloor apps remove webhook
```
---
### `teachfloor apps set oauth`
Configure the OAuth block. Presence of this block is the developer's explicit opt-in for install-integrated OAuth — see [OAuth](./oauth).
```bash
teachfloor apps set oauth
```
**Prompts** (interactive mode) / **Flags** (non-interactive):
- **Type** — `--oauthType ` (alias `--type`; currently only `install`)
**Example**:
```bash
$ teachfloor apps set oauth
✔ Select the OAuth grant type: install
✓ OAuth type set to "install".
```
**Updates Manifest**:
```json
{
"oauth": { "type": "install" }
}
```
:::info
OAuth credentials are delivered inside the `app.installed` webhook payload. If your app has no webhook block, no tokens will be minted even with `oauth` set. Run `apps set webhook` first (or after) to complete the setup.
:::
---
### `teachfloor apps remove oauth`
Strip the `oauth` block from the manifest. New installs will no longer receive an access token / refresh token pair on install. In-app SDK permissions still work.
```bash
teachfloor apps remove oauth
```
---
## Distribution
### `teachfloor apps set distribution`
Set app distribution type (public or private).
```bash
teachfloor apps set distribution
```
**Prompts** (interactive mode) / **Flags** (non-interactive):
- **Distribution type** — `--distributionType ` (alias `--type`; one of `private` or `public`)
**Distribution Types**:
- **private**: Only your organization (default)
- **public**: Listed in marketplace (requires review)
**Example**:
```bash
$ teachfloor apps set distribution
✔ Select distribution type: public
✓ Distribution type updated to public.
```
**Updates Manifest**:
```json
{
"distribution_type": "public"
}
```
**Important for Marketplace Submission**:
You **must** set distribution to `public` before submitting your app for marketplace review. Apps with `distribution_type: "private"` cannot be submitted to the public marketplace.
**Workflow for Public Apps**:
```bash
# 1. Set distribution to public
teachfloor apps set distribution
# Select: public
# 2. Upload your app
teachfloor apps upload
# 3. Submit via dashboard
# Navigate to Settings → Apps → Your App → Submit for Review
```
---
## Inspection
### `teachfloor apps show`
Print a spec-sheet summary of the current app — manifest metadata, webhook + OAuth config, permissions (with legacy-alias flagging), views, and install state on your own org. The quickest way to see "what does this app look like right now."
```bash
teachfloor apps show
```
**Options**:
- `-v, --verbose`: Expand widget declarations to show `id`/`name`/`description` per view (default is compact `Views (2 widgets, 1 drawer)`)
- `--json`: Emit machine-readable JSON instead of the pretty output (pipe into `jq`)
- `--no-remote`: Skip the permissions-catalog fetch; render local manifest only. Loses legacy-alias flagging and OAuth scope derivation but works fully offline
**What it prints**:
- **Header** — app name, version, id, distribution type
- **Metadata** — description + post-install action (only when set)
- **Webhook** — URL, subscribed events, note that lifecycle events (`app.installed` / `app.uninstalled`) are always delivered
- **OAuth** — type + "Scopes on install" (the OAuth scopes derived from the manifest's permissions, showing exactly what token an install would mint)
- **Permissions** — every entry with its purpose; legacy snake_case names show the canonical form with `(legacy alias: course_read)` inline
- **Views** — count by surface by default; expanded to per-view detail with `--verbose`
Sections only render when the corresponding manifest field is present.
**Example**:
```bash
$ teachfloor apps show
Webhook Test App v1.0.0
6a640834d241e private
Description Webhook test app
Post-install external → https://example.com
Webhook
URL https://webhook.site/5c1ab7fb-168a-4102-83a0-0425ffc073db
Events element.completed
(+ lifecycle: app.installed, app.uninstalled — always delivered)
OAuth
Type install
Scopes on install courses:read, elements:read, activities:read, members:read
Permissions (5)
courses:read Course permission
courses:read Read permission (legacy alias: course_read)
elements:read Element read permission (legacy alias: element_read)
activities:read Activity permission
members:read User read permission
```
**Scripting with `--json`**:
```bash
# List the OAuth scopes this app's install would mint
teachfloor apps show --json | jq -r '.computed.oauth_scopes_on_install[]'
# List every permission entry that's still using a legacy alias
teachfloor apps show --json | jq '.permissions[] | select(.is_legacy)'
```
---
## Diagnostics
### `teachfloor apps doctor`
Run a sequence of checks against your local setup + the current app, printing a pass/warn/fail line for each. Useful when something isn't working and you're not sure whether the problem is auth, the manifest, the app state on the platform, or your webhook/OAuth config.
```bash
teachfloor apps doctor
```
**Options**:
- `-v, --verbose`: Print the detail line for every check, not just non-passing ones
**What it checks**:
Environment
- **Authenticated** — token present, org selected, and `/whoami` still accepts it (the most common failure mode is a silently-expired token)
Manifest (only when run inside an app folder)
- **Inside app folder** — `teachfloor-app.json` exists
- **Manifest is valid JSON** — file parses
- **Manifest required fields** — `id`, `name`, `version` are all set
- **App exists on the platform** — the manifest's `id` resolves via `GET /apps/{id}`
Metadata (checked when the field is present)
- **Description** — warns when empty; marketplace listings render blank otherwise
- **Distribution type** — must be `private` or `public`
- **post_install_action** — shape check: `type` is required; when `type: "external"`, `url` is required and must be `https://`
Views (only when `ui_extension.views` is declared)
- **Views** — every view has `surface`, `viewport`, `component`; surface exists in the server catalog; widget-surface views additionally follow server rules (widget id matches `^[a-z][a-z0-9_]*$`, name ≤ 60 chars, description ≤ 200 chars, ids unique per app)
Permissions (only when declared)
- **Permissions** — every entry is `{ permission, purpose }`; each permission exists in the server catalog; legacy snake_case names (`course_read`) surface as warnings so you know to migrate to the canonical form (`courses:read`)
Webhook block (only when declared)
- **Webhook URL declared / uses HTTPS / length** — server-side rules mirrored locally so `apps upload` doesn't 422 on preventable issues
- **Webhook events subscribed** — every event in `webhook.events` is in the server's catalog
OAuth block (only when declared)
- **OAuth type is valid** — currently only `install` is accepted
- **OAuth prerequisites** — the three-prereq gate (`oauth` block + `webhook` block + at least one permission) — warns when credentials wouldn't actually be minted on install
**Exit code**: `0` when there are no errors (warnings still exit 0); `1` when any check failed.
**Example**:
```bash
$ teachfloor apps doctor
Running diagnostics...
✓ Authenticated
✓ Inside app folder
✓ Manifest is valid JSON
✓ Manifest required fields
✓ App exists on the platform
✓ Webhook URL — myapp.example.com
✓ Webhook events subscribed
⚠ OAuth prerequisites — no webhook block — credentials would have nowhere to land
7 passed, 1 warning
```
---
## Command Requirements
### Authentication Required
These commands require authentication:
- `teachfloor apps create`
- `teachfloor apps start`
- `teachfloor apps upload`
**Check authentication**:
```bash
teachfloor whoami
```
**Re-authenticate**:
```bash
teachfloor logout
teachfloor login
```
### App Folder Required
These commands must be run inside an app folder:
- `teachfloor apps start`
- `teachfloor apps upload`
- `teachfloor apps add view`
- `teachfloor apps remove view`
- `teachfloor apps add settings`
- `teachfloor apps add widget`
- `teachfloor apps remove widget`
- `teachfloor apps grant permission`
- `teachfloor apps revoke permission`
- `teachfloor apps set distribution`
**Check if in app folder**:
```bash
ls teachfloor-app.json
```
---
## Common Workflows
### Create and Test App
```bash
# 1. Install CLI
npm install -g @teachfloor/teachfloor-cli
# 2. Login
teachfloor login
# 3. Create app
teachfloor apps create my-app
cd my-app
# 4. Add a view
teachfloor apps add view
# 5. Start dev server
teachfloor apps start
# 6. Make changes and test
# (dev server auto-reloads)
# 7. Upload when ready
teachfloor apps upload
```
### Update Existing App
```bash
# 1. Navigate to app folder
cd my-app
# 2. Make code changes
# edit src/views/MyView.jsx
# 3. Update version in manifest
# Edit teachfloor-app.json: "version": "1.1.0"
# 4. Test locally
teachfloor apps start
# 5. Upload new version
teachfloor apps upload
```
### Add View to Existing App
```bash
cd my-app
# Add view
teachfloor apps add view
# Select viewport and enter component name
# Implement component
# edit src/views/NewView.jsx
# Test
teachfloor apps start
```
---
## Troubleshooting
### "Not logged in" error
**Solution**:
```bash
teachfloor login
```
### "Not in app folder" error
**Check for manifest**:
```bash
ls teachfloor-app.json
```
**Create new app if needed**:
```bash
teachfloor apps create my-app
cd my-app
```
### "Version already approved" error
**Solution**: Increment version in `teachfloor-app.json`:
```json
{
"version": "1.0.1"
}
```
### Build errors
**Clear cache**:
```bash
rm -rf node_modules
npm install
npm run build
```
---
## Getting Help
### Built-in Help
```bash
teachfloor --help
teachfloor apps --help
teachfloor apps create --help
```
### Version Info
```bash
teachfloor version
```
### Support Channels
- **Email**: support@teachfloor.com
- **Documentation**: [docs.teachfloor.com](https://docs.teachfloor.com)
---
## Next Steps
Learn about best practices:
→ Continue to [Best Practices](./best-practices)
## Additional Resources
- [Quickstart Guide](/docs/apps/quickstart)
- [App Manifest](/docs/apps/core-concepts/app-manifest)
- [Deployment](/docs/apps/advanced-topics/deployment)
---
## Document: /docs/apps/references/best-practices
URL: /docs/apps/references/best-practices
# Best Practices
A curated collection of best practices, patterns, and recommendations for building high-quality Teachfloor apps.
## Development
### Project Structure
Organize your code logically:
```
my-app/
├── src/
│ ├── index.js # Entry point
│ ├── views/ # Viewport components
│ │ ├── App.jsx
│ │ ├── CourseView.jsx
│ │ └── SettingsView.jsx
│ ├── components/ # Reusable components
│ │ ├── Header.jsx
│ │ └── shared/
│ ├── hooks/ # Custom hooks
│ │ ├── useAppData.js
│ │ └── usePreferences.js
│ ├── utils/ # Utilities
│ │ ├── api.js
│ │ └── formatting.js
│ └── constants/ # Constants
│ └── config.js
├── public/
└── teachfloor-app.json
```
### Component Design
**Do**: Create small, focused components
```javascript
// ✅ Good
function NotesList({ notes }) {
return notes.map(note => )
}
function NoteItem({ note }) {
return
}
```
### Debouncing
Debounce frequent operations:
```javascript
import { useCallback, useRef } from 'react'
function SearchInput() {
const timeoutRef = useRef(null)
const debouncedSearch = useCallback((query) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
timeoutRef.current = setTimeout(() => {
performSearch(query)
}, 500)
}, [])
return (
debouncedSearch(e.target.value)}
/>
)
}
```
### Bundle Size
Minimize bundle size:
```javascript
// ✅ Good: Import only what you need
import { Button, Text } from '@teachfloor/extension-kit'
// ❌ Bad: Import everything
import * as ExtensionKit from '@teachfloor/extension-kit'
```
### Caching
Implement data caching:
```javascript
const cache = new Map()
async function getCachedData(key, fetchFn, ttl = 60000) {
const cached = cache.get(key)
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.data
}
const data = await fetchFn()
cache.set(key, { data, timestamp: Date.now() })
return data
}
```
---
## Security
### Input Validation
Always validate user input:
```javascript
function validateEmail(email) {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return re.test(email)
}
function saveUserData(email, name) {
if (!validateEmail(email)) {
throw new Error('Invalid email format')
}
if (!name || name.length < 2) {
throw new Error('Name must be at least 2 characters')
}
// Save data
}
```
### Sanitization
Sanitize data before storage:
```javascript
function sanitizeString(str) {
return str
.replace(/
```
This will log SDK events to console.
---
## Common Mistakes
### 1. Not Handling Loading States
```javascript
// ❌ Bad
const { userContext } = useExtensionContext()
return
{userContext.name}
// ✅ Good
const { userContext, environment } = useExtensionContext()
if (!environment.initialized) return
return
{userContext.name}
```
### 2. Not Handling Async Operations
```javascript
// ❌ Bad
useEffect(async () => {
const data = await retrieve('key')
setData(data)
}, [])
// ✅ Good
useEffect(() => {
retrieve('key').then(setData)
}, [])
```
### 3. Over-Requesting Permissions
```javascript
// ❌ Bad - requesting everything
"permissions": [
{ "permission": "user:read", ... },
{ "permission": "user_events:read", ... },
{ "permission": "courses:read", ... },
// ... not all needed
]
// ✅ Good - only what's needed
"permissions": [
{ "permission": "courses:read", "purpose": "Display course info" }
]
```
### 4. Not Validating Data
```javascript
// ❌ Bad
await store('data', userInput, 'userdata')
// ✅ Good
if (!isValid(userInput)) {
throw new Error('Invalid input')
}
const sanitized = sanitize(userInput)
await store('data', sanitized, 'userdata')
```
---
## Quick Reference
### Reset Everything
```bash
# Logout
teachfloor logout
# Clear cache
rm -rf node_modules/.cache
rm -rf dist/
# Reinstall
rm -rf node_modules
npm install
# Login again
teachfloor login
# Rebuild
npm run build
```
### Debug Checklist
```
□ Check browser console for errors
□ Verify environment.initialized is true
□ Check manifest is valid JSON
□ Confirm permissions are listed
□ Verify component files exist
□ Check SDK script is loaded
□ Confirm app is installed
□ Check viewport matches page
□ Verify data storage permissions
□ Test with hard refresh
```
---
## Additional Resources
- [Quickstart Guide](/docs/apps/quickstart)
- [CLI Reference](./cli)
- [Best Practices](./best-practices)
- [Examples](./examples)
---
## Document: /docs/teachfloor-js
URL: /docs/teachfloor-js
# Teachfloor.js SDK
JavaScript SDK for integrating custom web pages with the Teachfloor platform.
## Overview
The Teachfloor.js SDK enables seamless communication between your custom web page and Teachfloor. Built on a promise-based architecture, it allows you to retrieve data, listen to events, and trigger actions within the Teachfloor platform.
## Quick Links
- [Getting Started](./teachfloor-js/getting-started)
- [SDK Methods](./teachfloor-js/sdk-methods)
- [Viewports Reference](./teachfloor-js/viewports-reference)
## Quick Start
```html
```
---
## Document: /docs/teachfloor-js/getting-started
URL: /docs/teachfloor-js/getting-started
# Getting Started
## Prerequisites
- A Teachfloor account with access to the Dashboard
- A web page where you'll integrate the SDK
## Step 1: Create a Teachfloor App
1. Log in to the [Teachfloor Dashboard](https://app.teachfloor.com)
2. Navigate to **Developers** → **Apps**
3. Click **Create App**
4. Provide:
- App name
- Icon and color theme
- URL where the SDK will be used
5. Save to generate your unique app ID
## Step 2: Install the SDK
Add the SDK script to your page's `` section:
```html
```
Replace `YOUR_UNIQUE_APP_ID` with the ID from your Teachfloor App.
## Step 3: Use the SDK
Once initialized, use the `API` object to interact with Teachfloor:
```javascript
tf('onInit', function(API) {
// Get authenticated user
API.get('auth.user')
.then(user => console.log('User:', user));
// Listen for viewport changes
API.on('environment.viewport.changed', (viewport) => {
console.log('Current viewport:', viewport);
});
});
```
## Next Steps
- Explore [SDK Methods](./sdk-methods)
- Review [Viewports Reference](./viewports-reference)
---
## Document: /docs/teachfloor-js/sdk-methods
URL: /docs/teachfloor-js/sdk-methods
# SDK Methods
The Teachfloor.js SDK provides five core methods for interacting with the platform.
## API.get()
Retrieve data from Teachfloor.
### Usage
```javascript
API.get(objectIdentifier)
```
### Parameters
- `objectIdentifier` (string): Identifier for the object to retrieve
### Available Objects
| Identifier | Description |
|------------|-------------|
| `auth.user` | Authenticated user details (ID, name, email, profile data) |
### Returns
Promise that resolves with the requested data or rejects on error.
### Example
```javascript
API.get('auth.user')
.then(user => console.log('User:', user))
.catch(error => console.error('Error:', error));
```
---
## API.on()
Listen for events within the Teachfloor platform.
### Usage
```javascript
API.on(eventIdentifier, callback)
```
### Parameters
- `eventIdentifier` (string): Event to listen for
- `callback` (function): Function called when event triggers
### Available Events
| Event | Description |
|-------|-------------|
| `environment.viewport.changed` | Viewport (page) changes within Teachfloor |
| `environment.path.changed` | URL path changes within Teachfloor |
### Example
```javascript
API.on('environment.viewport.changed', (viewport, objectContext) => {
if (viewport === 'teachfloor.dashboard.course.detail') {
console.log('Course page:', objectContext.course);
}
});
```
---
## API.emit()
Trigger actions within the Teachfloor platform.
### Usage
```javascript
API.emit(action, parameters)
```
### Parameters
- `action` (string): Action identifier
- `parameters` (object): Action configuration
### Available Actions
| Action | Description | Parameters |
|--------|-------------|------------|
| `ui.toast.show` | Display a toast notification | `message` (string, required) `autoClose` (number, optional) `color` (string, optional): `gray`, `green`, `red` |
| `ui.drawer.show` | Show the app drawer | None |
| `ui.drawer.hide` | Hide the app drawer | None |
| `ui.drawer.toggle` | Toggle the app drawer visibility | None |
### Example
```javascript
// Show toast notification
API.emit('ui.toast.show', {
message: 'Welcome to the course!',
autoClose: 3000,
color: 'green'
});
// Show drawer
API.emit('ui.drawer.show');
```
---
## API.set()
Store data in Teachfloor (internal use - available for first-party apps).
### Usage
```javascript
API.set(key, value, source)
```
### Parameters
- `key` (string): Storage key identifier
- `value` (any): Value to store
- `source` (string): Storage source - `appdata`, `userdata`, or `usercollection`
### Returns
Promise that resolves with the stored data or rejects on error.
---
## API.generate()
Generate AI content using Teachfloor's AI capabilities (beta feature).
### Usage
```javascript
API.generate(prompt, generationType)
```
### Parameters
- `prompt` (string): The generation prompt
- `generationType` (string): Type of generation (default: `ai/text-generate`)
### Returns
Promise that resolves with the generated content or rejects on error.
### Example
```javascript
API.generate('Write a course introduction', 'ai/text-generate')
.then(text => console.log('Generated:', text))
.catch(error => console.error('Error:', error));
```
---
## Summary
- **API.get()**: Retrieve data from Teachfloor
- **API.set()**: Store data (internal use)
- **API.on()**: Subscribe to platform events
- **API.emit()**: Trigger actions in Teachfloor
- **API.generate()**: Generate AI content (beta)
All methods use promises for asynchronous operations.
---
## Document: /docs/teachfloor-js/viewports-reference
URL: /docs/teachfloor-js/viewports-reference
# Viewports Reference
## Overview
A viewport identifies a specific page or view within the Teachfloor dashboard. Use viewports to execute context-specific logic based on where users are in the platform.
## Object Context
Some viewports provide an `objectContext` object containing details about the current page's associated objects (courses, modules, elements).
## Available Viewports
| Viewport ID | Path | Object Types |
|-------------|------|--------------|
| `teachfloor.dashboard.course.list` | `/:organization/courses` | - |
| `teachfloor.dashboard.course.detail` | `/:organization/courses/:course` | course |
| `teachfloor.dashboard.course.module.detail` | `/:organization/courses/:course/modules/:module` | course, module |
| `teachfloor.dashboard.course.element.detail` | `/:organization/courses/:course/modules/:module/elements/:element` | course, module, element |
| `teachfloor.dashboard.community.overview` | `/:organization/community` | - |
| `teachfloor.dashboard.community.channel.detail` | `/:organization/community/:channel` | channel |
| `teachfloor.dashboard.community.post.detail` | `/:organization/community/:channel/posts/:post` | channel, post |
| `teachfloor.dashboard.community.member.list` | `/:organization/community/:channel/members` | channel |
| `teachfloor.dashboard.community.event.list` | `/:organization/community/events` | - |
| `teachfloor.dashboard.community.event.detail` | `/:organization/community/events/:event` | event |
| `teachfloor.dashboard.settings.general.detail` | `/:organization/settings/general` | - |
| `teachfloor.dashboard.settings.customization.detail` | `/:organization/settings/customization` | - |
| `teachfloor.dashboard.settings.team.list` | `/:organization/settings/team` | - |
| `teachfloor.dashboard.settings.billing.detail` | `/:organization/settings/billing` | - |
| `teachfloor.dashboard.settings.integration.list` | `/:organization/settings/integrations` | - |
| `teachfloor.dashboard.settings.notification.list` | `/:organization/settings/notifications` | - |
| `teachfloor.dashboard.settings.custom-field.list` | `/:organization/settings/custom-fields` | - |
| `teachfloor.dashboard.account.detail` | `/:organization/account` | - |
| `teachfloor.dashboard.learner.list` | `/:organization/learners` | - |
| `teachfloor.dashboard.payment.list` | `/:organization/payments` | - |
## Usage Example
```javascript
API.on('environment.viewport.changed', (viewport, objectContext) => {
if (viewport === 'teachfloor.dashboard.course.detail') {
// Access course object
const course = objectContext.course;
console.log('Current course:', course);
}
});
```
---
## Document: Dev Blog
URL: /blog
# Dev Blog
Technical updates, integration tutorials, and behind-the-scenes looks at how we build Teachfloor.
---
---
## Document: Welcome to the Teachfloor Dev Blog
Introducing the Teachfloor developer blog — updates, tutorials, and deep dives into our platform.
URL: /blog/welcome-to-the-teachfloor-dev-blog
# Welcome to the Teachfloor Dev Blog
We're excited to launch the Teachfloor developer blog. This is where we'll share technical updates, integration tutorials, and behind-the-scenes looks at how we build Teachfloor.
## Why a dev blog?
As Teachfloor grows, so does our ecosystem. More teams are building integrations, automating workflows, and extending the platform with custom apps. We want to meet you where you are — with practical, hands-on content written by the people who build and maintain the platform.
This blog is for:
- **Developers** integrating Teachfloor into their stack
- **Partners** building apps on the Teachfloor platform
- **Technical teams** evaluating Teachfloor for their organization
## What to expect
We'll be publishing content across a few categories:
### API deep dives
Tips and patterns for getting the most out of the Teachfloor REST API — from authentication flows to pagination strategies, bulk operations, and error handling best practices.
### Webhook patterns
Real-world examples of event-driven integrations. How to set up reliable webhook consumers, handle retries, verify signatures, and react to course, member, and activity events.
### Extension Apps
Guides for building custom apps on the Teachfloor platform using the Extension Kit. We'll walk through viewports, components, data storage, and deployment — from first scaffold to production.
### Platform updates
Changelog-style posts covering new API endpoints, breaking changes, deprecation timelines, and migration guides. If something changes that affects your integration, you'll hear about it here first.
## Get involved
Have a question, a use case you'd like us to cover, or feedback on the docs? Reach out to us at [developers@teachfloor.com](mailto:developers@teachfloor.com) or open an issue on our [docs repo](https://github.com/teachfloor/docs).
We're just getting started — stay tuned.