Platform API
tRPC API reference
The frozen tRPC procedure inventory, access boundaries, current handler state, and shared schema shapes.
Transport and authentication
The API uses tRPC 11 over HTTP. Queries use GET and encode non-empty input in the
input query parameter; mutations use POST. Prefer a tRPC client so batching and
response envelopes remain correct.
const response = await fetch('https://api.facoolta.com/trpc/health.check', {
headers: { accept: 'application/json' },
});
if (!response.ok) throw new Error(`API returned ${response.status}`);
const envelope = await response.json();
Protected requests send Authorization: Bearer <access-token>. Verified
procedures additionally require current campus verification. Public procedures ignore the
absence of a session but still validate input.
Procedure status
The checked-in contract freezes 40 procedure paths. The live label means the
server contains a handler; it is not an availability or uptime claim. A stub
path is part of the contract but currently throws INTERNAL_SERVER_ERROR with the
message NOT_IMPLEMENTED.
Health and identity
| Procedure | Operation | Access | Input | Output | State |
|---|---|---|---|---|---|
health.check | query | public | void | Health | live |
auth.me | query | bearer | void | Me | live |
institutions.list | query | public | void | Institution[] | live |
institutions.get | query | public | { id: UUID } | Institution | live |
verification.status | query | bearer | void | VerificationStatus | live |
verification.submit | mutation | bearer | VerificationSubmit | — | stub |
Communities and realtime
| Procedure | Operation | Access | Input | Output | State |
|---|---|---|---|---|---|
communities.list | query | verified | void | Community[] | live |
communities.get | query | verified | { id: UUID } | Community | live |
communities.members | query | verified | { id: UUID } | CommunityMember[] | live |
discussions.list | query | verified | void | Discussion[] | live |
discussions.get | query | verified | { id: UUID } | Discussion | live |
talk.openDirect | mutation | verified | { userId: UUID } | Discussion | live |
talk.sync | mutation | verified | { discussionId: UUID } | { streamChannelId; memberCount } | live |
tokens.stream | query | verified | void | StreamToken | live |
tokens.livekit | mutation | verified | { discussionId: UUID } | LiveKitToken | live |
Resource lookups enforce membership and return NOT_FOUND when the object is absent
or inaccessible. The LiveKit token mutation checks discussion access before issuing a token.
talk.openDirect rejects self-messaging, unverified users, and blocked pairs;
talk.sync reconciles authorized discussion members with the Stream channel.
unidrive
| Procedure | Operation | Access | Input | Output | State |
|---|---|---|---|---|---|
unidrive.listShared | query | verified | void | DriveItem[] | live |
unidrive.get | query | verified | { id: UUID } | DriveItem | live |
unidrive.createShare | mutation | verified | CreateShare | — | stub |
unidrive.grant | mutation | verified | GrantAccess | — | stub |
unidrive.revoke | mutation | verified | RevokeAccess | — | stub |
unidrive.signedUrl | query | verified | { id: UUID } | SignedUrl | live |
Read operations enforce item access. unidrive.signedUrl requires read access and a
stored object path; its URL is temporary and includes an ISO expiry timestamp.
studate
| Procedure | Operation | Access | Input | Output | State |
|---|---|---|---|---|---|
studate.profile | query | verified | void | studateProfile | null | live |
studate.upsertProfile | mutation | verified | { age; bio; visible } | — | stub |
studate.discover | query | verified | void | studateProfile[] | live |
studate.like | mutation | verified | { userId: UUID } | — | stub |
studate.pass | mutation | verified | { userId: UUID } | — | stub |
studate.lock | query | verified | void | studateLock | null | live |
studate.continueLock | mutation | verified | void | studateLock | live |
studate.endLock | mutation | verified | void | void | live |
studate.setAvailability | mutation | verified | { blocks: Availability[] } | void | live |
studate.block | mutation | verified | { userId: UUID } | — | stub |
studate.report | mutation | verified | { userId; reason } | — | stub |
All studate procedures require a verified session. Profiles enforce age 18–99, bios are capped
at 500 characters, and availability accepts at most 40 blocks using HH:MM times.
The current discovery handler returns an empty list.
ginny and operations
| Procedure | Operation | Access | Input | Output | State |
|---|---|---|---|---|---|
ginny.complete | mutation | public | { installationId; prompt } | { text: string } | live |
ginny.enqueue | mutation | bearer | ginnyEnqueue | ginnyJob | live |
ginny.listJobs | query | bearer | void | ginnyJob[] | live |
ginny.getJob | query | bearer | { id: UUID } | ginnyJob | live |
push.register | mutation | bearer | PushRegistration | void | live |
push.unregister | mutation | bearer | PushRegistration | void | live |
reports.bug | mutation | bearer | { message; route? } | void | live |
backups.list | query | bearer | void | BackupMeta[] | live |
Push tokens are restricted to ios or android and 8–4096 characters.
Bug messages are 1–4000 characters; an optional route is capped at 200 characters.
ginny jobs accept the kinds summarize, study_pack, or
research; the optional opaque contextRef is capped at 2,000 characters.
Shared schemas
UUID fields use canonical UUID strings. Date-time fields use ISO 8601 date-time strings. The
excerpts below mirror the shared contract; import the schemas from @facoolta/contract
instead of duplicating them in workspace code.
type UUID = string;
type IsoDateTime = string;
type Institution = { id: UUID; slug: string; name: string };
type Me = {
userId: UUID;
email: string | null;
institutionId: UUID | null;
verification: 'none' | 'pending' | 'verified' | 'expired' | 'rejected';
};
type Community = {
id: UUID;
institutionId: UUID;
kind: 'course' | 'campus' | 'group';
slug: string;
title: string;
};
type Discussion = {
id: UUID;
communityId: UUID | null;
kind: 'course' | 'group' | 'direct' | 'studate';
streamChannelId: string;
createdAt: IsoDateTime;
}; type DriveItem = {
id: UUID; ownerId: UUID; parentId: UUID | null;
kind: 'file' | 'folder'; title: string; createdAt: IsoDateTime;
};
type studateProfile = { userId: UUID; age: number; bio: string; visible: boolean };
type studateLock = {
id: UUID; matchId: UUID; otherUserId: UUID;
phase: '3d' | '7d' | '30d' | '90d' | 'ongoing';
status: 'active' | 'decision' | 'ended';
lockedAt: IsoDateTime; lockEndsAt: IsoDateTime | null;
};
type Availability = { dayOfWeek: 0 | 1 | 2 | 3 | 4 | 5 | 6; startTime: string; endTime: string };
type ginnyJobKind = 'summarize' | 'study_pack' | 'research';
type ginnyEnqueue = {
title: string; // 1..200 characters
kind: ginnyJobKind;
contextRef?: string; // opaque reference, at most 2,000 characters
};
type ginnyJob = {
id: UUID; title: string; kind: ginnyJobKind;
status: 'queued' | 'running' | 'done' | 'needs_you';
triggerRunId: string | null; resultSummary: string | null;
createdAt: IsoDateTime;
}; - CreateShare
{ title: string(1..200); kind: file | folder; parentId?: UUID | null }- GrantAccess
{ itemId: UUID; access: read | write; exactly one of userId or communityId }- RevokeAccess
{ itemId: UUID; exactly one of userId or communityId }- VerificationSubmit
{ institutionId: UUID; method: lms_escrow | device_attested }
Errors
| Code | Meaning | Client action |
|---|---|---|
UNAUTHORIZED | Session is missing or invalid. | Refresh authentication once, then require sign-in. |
NOT_FOUND | Object is absent or the caller lacks access. | Do not reveal which case occurred. |
PRECONDITION_FAILED | The requested state transition is not allowed. | Refresh current state before retrying. |
TOO_MANY_REQUESTS | The ginny installation limit was reached. | Back off; do not rotate installation IDs. |
INTERNAL_SERVER_ERROR | Unexpected failure or an explicit stub. | Check for NOT_IMPLEMENTED; otherwise record request context. |