Focura API Reference
The Focura REST API lets you programmatically access workspaces, projects, tasks, focus sessions, files, notifications, and more. All endpoints return JSON. Authentication uses RS256 JWT Bearer tokens.
Base URL
https://focura-backend.onrender.com/apihttp://localhost:5000/apiAPI Reference
Authentication
Focura uses a dual-token RS256 JWT system. Short-lived access tokens are attached to requests; long-lived refresh tokens are stored HTTP-only and rotated on each use.
Obtain an access token
Call POST /api/auth/login with valid credentials. The response contains a short-lived RS256 JWT (15-minute expiry) in data.accessToken and sets an HTTP-only refresh token cookie.
POST /api/auth/login
Content-Type: application/json
{
"email": "you@example.com",
"password": "YourPassword123!"
}
// Response
{
"success": true,
"data": {
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": { "id": "...", "email": "...", "role": "USER" }
}
}Attach the token to requests
Include the access token in the Authorization header as a Bearer token on every authenticated request.
GET /api/workspaces
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/jsonSilent refresh before expiry
Access tokens expire in 15 minutes. Call POST /api/auth/refresh with the HTTP-only cookie to get a new access token. The refresh token is rotated on each call — the old one is revoked immediately in Redis.
// Recommended: refresh ~60 seconds before expiry
POST /api/auth/refresh
// (no body — refresh token is sent automatically via HTTP-only cookie)
// Response
{
"success": true,
"data": { "accessToken": "eyJhbGciOiJSUzI1NiIs..." }
}Handle 401 responses
If a request returns 401, attempt one silent refresh. If refresh also fails with 401, the session has been revoked — redirect the user to login.
// Axios interceptor pattern
axios.interceptors.response.use(null, async (error) => {
if (error.response?.status === 401 && !error.config._retry) {
error.config._retry = true;
try {
const { data } = await axios.post('/api/auth/refresh', {},
{ withCredentials: true }
);
const newToken = data.data.accessToken;
error.config.headers['Authorization'] = `Bearer ${newToken}`;
return axios(error.config);
} catch {
// Refresh failed — session fully expired
window.location.href = '/login';
}
}
return Promise.reject(error);
});JWT Access Token Payload
{
"sub" : "cm_user_abc123", // User cuid
"email": "you@example.com",
"role" : "USER", // USER | ADMIN | SUPER_ADMIN
"iat" : 1714480000, // Issued at (Unix timestamp)
"exp" : 1714480900, // Expires at (iat + 900s = 15 min)
"jti" : "unique-jwt-id" // JWT ID — tracked in Redis for revocation
}Never store access tokens in localStorage. They are kept in memory only. The refresh token is HTTP-only and inaccessible to JavaScript.
Token revocation: Each JWT has a unique JTI tracked in Upstash Redis. Logout, password change, and role updates immediately invalidate all active tokens via Redis TTL.
Algorithm: RS256 (asymmetric). The backend signs with a private key; the frontend verifies with the public key. HMAC (HS256) is not used for user tokens.
Rate Limits
Focura enforces per-endpoint rate limits via Upstash Redis sliding window counters. Limits are applied per IP for public routes and per user for authenticated routes.
Limit Tiers by Endpoint
| Endpoint | Primary Limit | Secondary Limit | Backend | Auth | On Exceeded |
|---|---|---|---|---|---|
POST /api/contact | 3 / hour per IP | 2 / 24 h per email | Upstash Redis sliding window | Public | 429 + retryAfter timestamp |
POST /api/files/upload | 10 uploads / 10 min per user | Plan file size limit | Upstash Redis + DB quota check | Auth | 429 or 413 / 507 |
POST /api/auth/login | 10 attempts / 15 min per IP | — | Upstash Redis | Public | 429 with lockout duration |
POST /api/auth/register | 5 / hour per IP | — | Upstash Redis | Public | 429 |
All other endpoints | 300 / min per user | — | Express global limiter | Auth | 429 |
Rate Limit Response Headers
X-RateLimit-LimitMaximum requests allowed in the current window
X-RateLimit-RemainingRequests remaining in the current window
X-RateLimit-ResetUnix timestamp (ms) when the window resets
Retry-AfterSeconds to wait before retrying (only on 429)
429 Response Shape
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit : 3
X-RateLimit-Remaining: 0
X-RateLimit-Reset : 1714483600000
Retry-After : 3600
{
"success" : false,
"error" : "TOO_MANY_REQUESTS",
"message" : "You have sent too many messages. Please try again in 60 minutes.",
"retryAfter": 1714483600000,
"remaining" : 0
}Rate limit state is stored in Upstash Redis with automatic TTL expiry — no manual cleanup required. Limits are enforced at the middleware layer before any business logic runs, so rejected requests do not create DB records.
Real-time Events (SSE)
Focura uses Server-Sent Events for real-time push notifications. Each authenticated user maintains a persistent GET connection to the stream endpoint. No WebSocket infrastructure required.
Protocol
Server-Sent Events (SSE)Endpoint
GET /api/notifications/streamContent-Type
text/event-streamSSE Event Shape
// Raw SSE stream from the server:
event: notification
data: {
"id" : "cm_notif_abc123",
"type" : "TASK_ASSIGNED",
"title" : "Task assigned to you",
"message" : "Raihan assigned 'Implement dark mode' to you",
"read" : false,
"actionUrl": "/workspace/cm_ws_xyz/tasks/cm_task_abc",
"createdAt": "2026-04-30T14:22:00.000Z",
"sender" : {
"id" : "cm_user_raihan",
"name" : "Mohammad Raihan",
"image": "https://res.cloudinary.com/..."
}
}Notification Event Types (18 total)
TASK_ASSIGNEDA task was assigned to you
TASK_COMPLETEDA task you created or are assigned to was completed
TASK_COMMENTEDA comment was added to a task you're involved with
MENTIONYou were @mentioned in a comment
TASK_DUE_SOONA task assigned to you is due within 24 hours
TASK_OVERDUEA task assigned to you has passed its due date
MEMBER_JOINEDA new member joined a workspace you belong to
MEMBER_REMOVEDA member was removed from the workspace
ROLE_UPDATEDYour workspace role was changed
WORKSPACE_INVITEYou received a workspace invitation
PROJECT_UPDATEA project you're a member of was updated
FILE_SHAREDA file was uploaded to a project or task you are on
MEETING_CREATEDA new meeting was created in your workspace
MEETING_UPDATEDA meeting you're attending was updated
MEETING_CANCELLEDA meeting you're attending was cancelled
MEETING_REMINDERA meeting you're attending starts in 15 minutes
DEADLINE_REMINDERA task deadline reminder
ANNOUNCEMENTA workspace announcement was posted
React + TanStack Query Integration
// hooks/useNotifications.ts
import { useEffect, useCallback } from 'react';
import { useQueryClient } from '@tanstack/react-query';
const API = process.env.NEXT_PUBLIC_API_URL ?? '';
export function useNotifications() {
const queryClient = useQueryClient();
const handleEvent = useCallback((event: MessageEvent) => {
const notification = JSON.parse(event.data);
// 1. Optimistically add to local notification list
queryClient.setQueryData(['notifications'], (old: any) => ({
...old,
data: {
...old?.data,
notifications: [notification, ...(old?.data?.notifications ?? [])],
},
}));
// 2. Invalidate relevant queries based on notification type
if (['TASK_ASSIGNED', 'TASK_COMPLETED'].includes(notification.type)) {
queryClient.invalidateQueries({ queryKey: ['tasks'] });
}
if (notification.type === 'MEMBER_JOINED') {
queryClient.invalidateQueries({ queryKey: ['workspace-members'] });
}
}, [queryClient]);
useEffect(() => {
const es = new EventSource(`${API}/api/notifications/stream`, {
withCredentials: true, // sends HTTP-only auth cookie
});
es.addEventListener('notification', handleEvent);
es.onerror = () => {
// Browser auto-reconnects with exponential backoff
console.warn('SSE connection error — reconnecting…');
};
return () => {
es.removeEventListener('notification', handleEvent);
es.close();
};
}, [handleEvent]);
}Corporate firewalls and VPNs may block persistent HTTP connections. If SSE fails, the browser will attempt automatic reconnection with exponential backoff.
Connection limits: Browsers cap SSE connections per domain at 6 (HTTP/1.1) or unlimited (HTTP/2). Focura runs on HTTP/2 in production on Render.com.
Errors
All error responses follow a consistent JSON shape. Use the error field for programmatic handling and message for display.
HTTP Status Codes
200OK
Request succeeded. Body contains data.
201Created
Resource created. Body contains the new resource.
400Bad Request
The request is malformed — usually a missing required field or invalid value.
401Unauthorized
Missing, expired, or revoked access token. Attempt silent refresh.
403Forbidden
Token is valid but the caller lacks the required role or ownership.
404Not Found
The requested resource does not exist or the caller cannot see it.
409Conflict
A uniqueness constraint was violated (e.g. email already registered).
413Payload Too Large
Uploaded file exceeds the plan file size limit.
422Unprocessable Entity
Validation failed. The errors field contains field-level detail.
429Too Many Requests
Rate limit exceeded. See Retry-After header or retryAfter field.
500Internal Server Error
Unexpected server error. These are logged — contact support if persistent.
507Insufficient Storage
Workspace storage quota exceeded. Delete files or upgrade plan.
Error Response Shape
// All error responses follow this shape:
{
"success": false,
"error" : "ERROR_CODE", // Machine-readable constant (snake_case uppercase)
"message": "Human explanation" // User-facing message — safe to display
}
// 422 Validation errors additionally include:
{
"success": false,
"error" : "VALIDATION_ERROR",
"message": "Please fix the errors below.",
"errors" : {
"email" : ["Please enter a valid email address"],
"password": ["Min 8 characters"],
"name" : ["Name must be at least 2 characters"]
}
}
// 429 Rate limit errors additionally include:
{
"success" : false,
"error" : "TOO_MANY_REQUESTS",
"message" : "You have sent too many messages. Please try again in 60 minutes.",
"retryAfter": 1714483600000, // Unix ms — when the window resets
"remaining" : 0
}Machine-readable Error Codes
VALIDATION_ERROROne or more request fields failed Zod schema validation
TOO_MANY_REQUESTSRate limit exceeded (IP or email based)
UNAUTHORIZEDNo valid access token provided
FORBIDDENToken valid but role insufficient
NOT_FOUNDResource does not exist or is not accessible
CONFLICTUniqueness constraint violated
INVALID_STATUSStatus value not in allowed enum
PLAN_LIMIT_REACHEDWorkspace plan limit (members, projects, storage) exceeded
STORAGE_LIMIT_REACHEDWorkspace storage quota exhausted
FILE_TOO_LARGEUpload exceeds plan file size limit
UPLOAD_RATE_LIMITToo many uploads in a short window
INTERNAL_ERRORUnhandled server exception — safe fallback message returned
8 endpoints
Authentication
Register, login, logout, token exchange, refresh, and session management. Focura uses a dual-token system: NextAuth issues a session on the frontend, which is exchanged for a backend RS256 JWT via HMAC proof. All subsequent API requests use that JWT in the Authorization header.
Creates a new user account. Sends a verification email. Returns the user object (no token — login separately).
curl -X POST https://focura-backend.onrender.com/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"name": "Mohammad Raihan",
"email": "raihan@example.com",
"password": "SecurePass123!"
}'7 endpoints
Workspaces
Create and manage workspaces — the top-level container for all projects, tasks, and team members in Focura.
5 endpoints
Projects
Projects group tasks within a workspace. Each project has its own views, members, milestones, and analytics.
6 endpoints
Tasks
Full CRUD for tasks, subtasks, dependencies, assignments, and time entries. Tasks are the core entity in Focura.
3 endpoints
Focus Sessions
Manage Pomodoro, deep work, and custom focus sessions. Completed sessions are logged for analytics and optional task time-tracking.
3 endpoints
Notifications
Real-time notifications via SSE. The /stream endpoint opens a persistent connection that pushes events. REST endpoints manage the notification inbox.
3 endpoints
Files & Attachments
File upload and management via Cloudinary. All uploads are rate-limited per user and subject to workspace storage quotas.
1 endpoint
Contact
Public contact form submission. Rate-limited per IP and email. Saves to DB and dispatches both an admin notification email and a user auto-reply.
2 endpoints
Job Postings (Careers)
Public read endpoints for the careers page. Write endpoints are admin-only, gated by FOCURA_ADMIN_IDS env var.
Missing something?
If you need an endpoint that isn't documented here or you found an issue with the docs, let us know.
2 endpoints
Comments
Task-scoped comments with @mention support. Creating a comment triggers real-time SSE notifications to all task assignees and the task creator.