Overview
The User API provides endpoints for retrieving a user’s document library and submitting feedback to the platform.
Procedures
getUsersDocs
Retrieve all documents accessible to the current user, including owned documents and documents shared via collaboration.
No input parameters required - uses authenticated session
Returns: Array of documents sorted by most recently updated
Document unique identifier
Whether the document has been vectorized for AI chat
Whether this is a collaborated document (true) or owned document (false)
URL to the document’s cover image/thumbnail
Total number of pages in the document
The last page the user was reading (1-indexed)
Example:
import { api } from "@/lib/api" ;
function DocumentLibrary () {
const { data : documents , isLoading } = api . user . getUsersDocs . useQuery ();
if ( isLoading ) return < div > Loading your library ...</ div > ;
return (
< div className = "document-grid" >
{ documents ?. map (( doc ) => (
< div key = {doc. id } className = "document-card" >
< img src = {doc. coverImageUrl } alt = {doc. title } />
< h3 >{doc. title } </ h3 >
< div className = "meta" >
< span >{doc. pageCount } pages </ span >
{ doc . isCollab && < span className = "badge" > Shared </ span > }
{ doc . isVectorised && < span className = "badge" > AI Ready </ span > }
</ div >
< progress
value = {doc. lastReadPage }
max = {doc. pageCount }
/>
< span >{Math.round((doc.lastReadPage / doc.pageCount) * 100 ) } % read </ span >
</ div >
))}
</ div >
);
}
Document Sorting
Documents are automatically sorted by updatedAt timestamp (most recent first). This includes:
Recently edited documents
Documents with new highlights or messages
Documents with updated reading progress
submitFeedback
Submit user feedback to the platform. This is a public procedure that can be used by authenticated or unauthenticated users.
The feedback message content
Type of feedback (e.g., “bug”, “feature”, “general”)
Contact email (optional, for anonymous users or if different from account email)
Returns: The created feedback object
Example:
Authenticated User
Anonymous User
import { api } from "@/lib/api" ;
function FeedbackForm () {
const [ message , setMessage ] = useState ( "" );
const [ type , setType ] = useState ( "general" );
const submitFeedback = api . user . submitFeedback . useMutation ({
onSuccess : () => {
toast . success ( "Thank you for your feedback!" );
setMessage ( "" );
},
});
const handleSubmit = ( e : React . FormEvent ) => {
e . preventDefault ();
submitFeedback . mutate ({ message , type });
};
return (
< form onSubmit = { handleSubmit } >
< select value = { type } onChange = { e => setType ( e . target . value )} >
< option value = "bug" > Bug Report </ option >
< option value = "feature" > Feature Request </ option >
< option value = "general" > General Feedback </ option >
</ select >
< textarea
value = { message }
onChange = { e => setMessage ( e . target . value )}
placeholder = "Tell us what you think..."
required
/>
< button type = "submit" disabled = {submitFeedback. isLoading } >
Submit Feedback
</ button >
</ form >
);
}
Data Models
User Schema
interface User {
id : string ;
name : string ;
email : string | null ;
emailVerified : Date | null ;
image : string | null ;
createdAt : Date ;
plan : Plan ;
}
enum Plan {
FREE = "FREE" ,
FREE_PLUS = "FREE_PLUS" ,
PRO = "PRO"
}
Feedback Schema
interface Feedback {
id : string ;
message : string ;
contact_email : string | null ;
type : string ;
createdAt : Date ;
userId : string | null ; // null for anonymous feedback
}
Use Cases
Dashboard Document List
function Dashboard () {
const { data : documents } = api . user . getUsersDocs . useQuery ();
const ownedDocs = documents ?. filter ( d => ! d . isCollab ) ?? [];
const sharedDocs = documents ?. filter ( d => d . isCollab ) ?? [];
return (
< div >
< section >
< h2 > My Documents ({ownedDocs. length }) </ h2 >
< DocumentGrid documents = { ownedDocs } />
</ section >
< section >
< h2 > Shared with Me ({sharedDocs. length }) </ h2 >
< DocumentGrid documents = { sharedDocs } />
</ section >
</ div >
);
}
Reading Progress
function ReadingProgress () {
const { data : documents } = api . user . getUsersDocs . useQuery ();
const inProgress = documents ?. filter ( doc =>
doc . lastReadPage > 1 && doc . lastReadPage < doc . pageCount
) ?? [];
return (
< div >
< h2 > Continue Reading </ h2 >
{ inProgress . map ( doc => (
< div key = {doc. id } >
< h3 >{doc. title } </ h3 >
< p > Page { doc . lastReadPage } of { doc . pageCount } </ p >
< ProgressBar
value = {doc. lastReadPage }
max = {doc. pageCount }
/>
< Link href = { `/document/ ${ doc . id } ` } > Continue Reading </ Link >
</ div >
))}
</ div >
);
}
Vectorization Status
function VectorizationStatus () {
const { data : documents } = api . user . getUsersDocs . useQuery ();
const needsVectorization = documents ?. filter ( d => ! d . isVectorised ) ?? [];
return (
< div >
{ needsVectorization . length > 0 && (
< Alert >
< h3 > Enable AI Chat </ h3 >
< p >
{ needsVectorization . length } document ( s ) can be vectorized for AI chat .
</ p >
< ul >
{ needsVectorization . map ( doc => (
< li key = {doc. id } >
{ doc . title }
< VectorizeButton documentId = {doc. id } />
</ li >
))}
</ ul >
</ Alert >
)}
</ div >
);
}
Search and Filter
function DocumentSearch () {
const { data : documents } = api . user . getUsersDocs . useQuery ();
const [ search , setSearch ] = useState ( "" );
const [ filter , setFilter ] = useState < 'all' | 'owned' | 'shared' >( 'all' );
const filteredDocs = documents ?. filter ( doc => {
const matchesSearch = doc . title . toLowerCase (). includes ( search . toLowerCase ());
const matchesFilter =
filter === 'all' ? true :
filter === 'owned' ? ! doc . isCollab :
doc . isCollab ;
return matchesSearch && matchesFilter ;
});
return (
< div >
< input
type = "search"
value = { search }
onChange = { e => setSearch ( e . target . value )}
placeholder = "Search documents..."
/>
< FilterButtons >
< button onClick = {() => setFilter ( 'all' )} > All </ button >
< button onClick = {() => setFilter ( 'owned' )} > My Documents </ button >
< button onClick = {() => setFilter ( 'shared' )} > Shared </ button >
</ FilterButtons >
< DocumentGrid documents = { filteredDocs } />
</ div >
);
}
Caching The document list is cached by React Query. It automatically refetches when:
Component remounts after being unmounted
Window regains focus
Network reconnects
Manually invalidate when documents change: const utils = api . useContext ();
utils . user . getUsersDocs . invalidate ();
Optimistic Updates Update the cache optimistically when modifying documents: const updateTitle = api . document . updateTitle . useMutation ({
onMutate : async ( newData ) => {
await utils . user . getUsersDocs . cancel ();
const previousDocs = utils . user . getUsersDocs . getData ();
utils . user . getUsersDocs . setData ( undefined , ( old ) =>
old ?. map ( doc =>
doc . id === newData . docId
? { ... doc , title: newData . title }
: doc
)
);
return { previousDocs };
},
});
Error Handling
const { data , error , isError } = api . user . getUsersDocs . useQuery ( undefined , {
onError : ( error ) => {
if ( error . data ?. code === "UNAUTHORIZED" ) {
// User needs to log in
router . push ( "/login" );
} else {
toast . error ( "Failed to load documents" );
}
},
retry: 3 , // Retry failed requests
});