REST API · v1 · Live

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.

REST over HTTPSRS256 JWT AuthSSE Real-time

Base URL

Production
https://focura-backend.onrender.com/api
Local dev
http://localhost:5000/api
API Versionv1
Methods:GETPOSTPUTPATCHDELETE

API 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.

01

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" }
  }
}
02

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/json
03

Silent 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..." }
}
04

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

EndpointPrimary LimitSecondary LimitBackendAuthOn Exceeded
POST /api/contact3 / hour per IP2 / 24 h per emailUpstash Redis sliding windowPublic429 + retryAfter timestamp
POST /api/files/upload10 uploads / 10 min per userPlan file size limitUpstash Redis + DB quota checkAuth429 or 413 / 507
POST /api/auth/login10 attempts / 15 min per IPUpstash RedisPublic429 with lockout duration
POST /api/auth/register5 / hour per IPUpstash RedisPublic429
All other endpoints300 / min per userExpress global limiterAuth429

Rate Limit Response Headers

X-RateLimit-Limit

Maximum requests allowed in the current window

X-RateLimit-Remaining

Requests remaining in the current window

X-RateLimit-Reset

Unix timestamp (ms) when the window resets

Retry-After

Seconds 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/stream

Content-Type

text/event-stream

SSE 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_ASSIGNED

A task was assigned to you

TASK_COMPLETED

A task you created or are assigned to was completed

TASK_COMMENTED

A comment was added to a task you're involved with

MENTION

You were @mentioned in a comment

TASK_DUE_SOON

A task assigned to you is due within 24 hours

TASK_OVERDUE

A task assigned to you has passed its due date

MEMBER_JOINED

A new member joined a workspace you belong to

MEMBER_REMOVED

A member was removed from the workspace

ROLE_UPDATED

Your workspace role was changed

WORKSPACE_INVITE

You received a workspace invitation

PROJECT_UPDATE

A project you're a member of was updated

FILE_SHARED

A file was uploaded to a project or task you are on

MEETING_CREATED

A new meeting was created in your workspace

MEETING_UPDATED

A meeting you're attending was updated

MEETING_CANCELLED

A meeting you're attending was cancelled

MEETING_REMINDER

A meeting you're attending starts in 15 minutes

DEADLINE_REMINDER

A task deadline reminder

ANNOUNCEMENT

A 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

200

OK

Request succeeded. Body contains data.

201

Created

Resource created. Body contains the new resource.

400

Bad Request

The request is malformed — usually a missing required field or invalid value.

401

Unauthorized

Missing, expired, or revoked access token. Attempt silent refresh.

403

Forbidden

Token is valid but the caller lacks the required role or ownership.

404

Not Found

The requested resource does not exist or the caller cannot see it.

409

Conflict

A uniqueness constraint was violated (e.g. email already registered).

413

Payload Too Large

Uploaded file exceeds the plan file size limit.

422

Unprocessable Entity

Validation failed. The errors field contains field-level detail.

429

Too Many Requests

Rate limit exceeded. See Retry-After header or retryAfter field.

500

Internal Server Error

Unexpected server error. These are logged — contact support if persistent.

507

Insufficient 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_ERROR

One or more request fields failed Zod schema validation

TOO_MANY_REQUESTS

Rate limit exceeded (IP or email based)

UNAUTHORIZED

No valid access token provided

FORBIDDEN

Token valid but role insufficient

NOT_FOUND

Resource does not exist or is not accessible

CONFLICT

Uniqueness constraint violated

INVALID_STATUS

Status value not in allowed enum

PLAN_LIMIT_REACHED

Workspace plan limit (members, projects, storage) exceeded

STORAGE_LIMIT_REACHED

Workspace storage quota exhausted

FILE_TOO_LARGE

Upload exceeds plan file size limit

UPLOAD_RATE_LIMIT

Too many uploads in a short window

INTERNAL_ERROR

Unhandled 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.

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.

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.