Skip to main content

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[]
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.
message
string
required
The feedback message content
type
string
required
Type of feedback (e.g., “bug”, “feature”, “general”)
email
string
Contact email (optional, for anonymous users or if different from account email)
Returns: The created feedback object Example:
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>
  );
}

Performance Considerations

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
});

Build docs developers (and LLMs) love