Skip to content
TALA
Esc
navigateopen⌘Jpreview
On this page

Data shapes

Every domain interface the SPA receives from api/tala.ts, what each endpoint returns, and the three shapes that mislead you if you skim them.

These are the types the SPA actually works in, all exported from src/api/tala.ts. They describe what comes back after the envelope is unwrapped — api<T>() returns .data, so a function typed Promise<Project[]> resolves to the array, not to { message, data }.

Unions

export type Scope = "personal" | "organization";
export type WorkStatus = "QUEUED" | "IN_PROGRESS" | "COMPLETED";
export type WorkPriority = "LOW" | "MEDIUM" | "HIGH";
export type OrgRole = "OWNER" | "ADMIN" | "MEMBER";

WorkStatus and WorkPriority are shared by projects and tasks — the backend backs both with the same Postgres enum types, so a value added for one applies to both.

Organizations

interface Organization {
  id: string;
  name: string;
  slug: string;
  role: OrgRole;          // the caller's role, not the org's
}

interface Member {
  id: string;             // the membership id, NOT the user id
  role: OrgRole;
  status: string;
  user: { id: string; fullName?: string; email: string; imageUrl?: string };
}

interface Invitation {
  id: string;
  email: string;
  role: OrgRole;
  expiresAt: string;
  acceptedAt?: string;
  token?: string;
  organization?: Organization;
}

Member.status is typed string, not a union, even though the backend only ever sends ACTIVE, INVITED, or SUSPENDED. Narrow it yourself if you branch on it.

Invitation.organization is populated by GET /organizations/invitations/:token so the invite screen can name the organization before the user commits. It is absent on the invitations returned inside GET /organizations/:id/members.

Work items

interface Project {
  id: string;
  name: string;
  description?: string;
  status: WorkStatus;
  priority: WorkPriority;
  startDate?: string;     // date only
  dueDate?: string;       // date only
  position: number;
  progress: number;
}

interface Task {
  id: string;
  title: string;
  description?: string;
  status: WorkStatus;
  priority: WorkPriority;
  startAt?: string;       // timestamp
  dueAt?: string;         // timestamp
  position: number;
  project: Project;       // always expanded, never an id
  assignees: Member[];    // memberships, not users
}

interface TaskComment {
  id: string;
  body: string;
  author: { id: string; fullName?: string; email: string };
  createdAt: string;
  assets: AssetDetail[];
}

Projects carry startDate / dueDate as dates; tasks carry startAt / dueAt as timestamps. The columns differ in the database too, so do not format them with the same helper.

position is the board ordering within a status column. Move a card with PATCH /projects/:id/position or PATCH /tasks/:id/position, sending both the new status and the new position — the second is not inferred from the first.

Library

interface Collection {
  id: string;
  title: string;
  description?: string;
  isFavorite?: boolean;
}

interface AssetDetail {
  id: string;
  assetName: string;
  assetType: string;      // MIME type, e.g. "image/png"
  currentVersion: number;
  project?: Project;
  isFavorite?: boolean;
}

interface AssetVersion {
  id: string;
  versionNumber: number;
  changeNote: string | null;
  createdAt: string;
}

isFavorite is computed per caller, not stored on the row — the backend joins asset_favorites / collection_favorites for the requesting user. It is optional because endpoints that do not perform that join omit it rather than sending false.

Billing and dashboard

interface Plan {
  id: string;
  name: string;
  minimumSeat: number;
  maximumSeat: number;
}

interface DashboardSummary {
  projectCount: number;
  dueTaskCount: number;   // not COMPLETED, due within 7 days
  memberCount: number;    // ACTIVE memberships only
  recentActivity: {
    id: string;
    title: string;
    projectName: string;
    status: WorkStatus;
    occurredAt: string;
  }[];
}

maximumSeat on the organization owner’s plan is what the seat limit is enforced against — see Organizations.

recentActivity is the eight most recently updated tasks from the last 30 days. Despite the name it is not the audit log; that is /api/timeline, which has no consumer.

Every function

Function Calls
organizations() GET /organizations
createOrganization(name) POST /organizations
organizationMembers(id) GET /organizations/:id/members{ members, invitations }
inviteMember(id, email, role) POST /organizations/:id/invitations
getInvitation(token) GET /organizations/invitations/:token
acceptInvitation(token) POST /organizations/invitations/:token/accept
updateMember(orgId, memberId, role) PATCH /organizations/:orgId/members/:memberId
removeMember(orgId, memberId) DELETE /organizations/:orgId/members/:memberId
projects() GET /projects
createProject(input) POST /projects
moveProject(id, status, position) PATCH /projects/:id/position
tasks(params) GET /tasks?from&to&projectId&status&assigneeId
projectTasks(projectId) GET /projects/:projectId/tasks
createTask(projectId, input) POST /projects/:projectId/tasks
setTaskAssignees(taskId, memberIds) PUT /tasks/:id/assignees
taskComments(taskId) GET /tasks/:taskId/comments
createTaskComment(taskId, body, assetIds) POST /tasks/:taskId/comments
collections(scope, q) GET /collections?scope&q
createCollection(input) POST /collections/create
favoriteCollection(id, favorite) PATCH /collections/:id/favoriteexported, never called
assets(scope, q) GET /assets?scope&q
favoriteAsset(id, favorite) PATCH /assets/:id/favorite
downloadAsset(id) GET /assets/:id/download{ url }
publishAsset(id, input) POST /assets/:id/publish
getAssetVersions(id) GET /assets/:id/versionsexported, never called
restoreAssetVersion(id, n) PATCH /assets/:id/versions/restore/:n
updateProfile(fullName) PATCH /auth/user/profile
startAvatarUpload(contentType) POST /auth/user/avatar-upload{ key, uploadUrl }
completeAvatarUpload(key) POST /auth/user/avatar-upload/complete
getPlans() GET /plan
dashboardSummary() GET /dashboard/summary
verifyPasswordResetToken(token) GET /auth/verify-token?token

The two marked exported, never called are wrappers with no caller. favoriteCollection is the server-backed version of a favourite that the UI currently keeps only in localStorage; getAssetVersions is the listing that version restore ships without. Wiring either is a small change — see What’s wired.

Presigned URL lifetimes

Three different windows, none of them configurable from the client:

URL Valid for From
Multipart upload part 1 hour POST /api/upload/initiate
Avatar upload 15 minutes POST /api/auth/user/avatar-upload
Asset download 5 minutes GET /api/assets/:id/download

Fetch a download URL at the moment of use. Putting one in component state and rendering it later is the common way to ship a broken image.

Was this page helpful?