Overview

The RM Pocket Lawyer backend runs on AWS Amplify Gen 2 with AppSync (GraphQL) as the API layer. All operations are accessed through a typed Amplify Data client — there are no REST endpoints. Lambda functions back the custom queries and mutations.

StackDetails
API LayerAWS AppSync (GraphQL)
AuthAmazon Cognito User Pools (email login, optional TOTP MFA)
DatabaseDynamoDB (auto-managed by Amplify Data models)
FunctionsAWS Lambda (Node.js 18, TypeScript)
FrontendReact Native (Expo 54) + Expo Router
Client SDKaws-amplify v6 — generateClient<Schema>()
All API calls go through the Amplify Data client — not raw HTTP. The client handles auth tokens, retries, and GraphQL encoding automatically.

Authentication

The default authorization mode is Cognito User Pool (JWT). Some public endpoints also accept an API Key for unauthenticated access.

User Groups (Roles)

GroupDescriptionAssigned By
ProspectDefault role on sign-up. Can use AI chat and submit intakes.Auto (post-confirmation trigger)
ClientActive legal client. Can view case summaries and documents.Admin via assignUserRole
StaffFirm employee. Full CRUD on cases, intakes, callbacks.Admin via assignUserRole
AdminSuperuser. Can manage roles, invite users, and access all data.Admin via assignUserRole

User Attributes (Cognito)

AttributeRequiredMutable
emailYes (login)No
given_nameYesYes
family_nameYesYes
phone_numberNoYes

Client Setup

How to import and use the typed Amplify Data client in your frontend code.

1. Configure Amplify (already done in lib/amplify.ts)

import { Amplify } from 'aws-amplify';
import { generateClient } from 'aws-amplify/data';
import type { Schema } from '@/amplify/data/resource';
import outputs from '../amplify_outputs.json';

Amplify.configure(outputs);

export const dataClient = generateClient<Schema>();

2. Use the client in any screen/component

import { dataClient } from '@/lib/amplify';

// CRUD on a model
const { data: profiles } = await dataClient.models.UserProfile.list();

// Custom query
const { data: reply } = await dataClient.queries.generateAiResponse({
  message: "What should I do after a car accident?"
});

// Custom mutation
const { data: result } = await dataClient.mutations.assignUserRole({
  targetUserId: "abc-123",
  targetRole: "Client"
});
The client auto-attaches the Cognito JWT. You do not need to pass auth headers manually. If the user is not signed in, calls requiring authenticated() will throw an UnauthorizedException.

Custom Queries (Lambda-backed)

generateAiResponse

Query generateAiResponse

Send a message to the "Pocket Lawyer" AI assistant. Returns a plain-text response string. The AI is backed by OpenAI GPT-4o-mini and scoped to personal injury / insurance legal information (not legal advice).

authenticated guest

Arguments

NameTypeRequiredDescription
messageStringYesThe user's question or message
history[AiHistoryMessage]No Previous conversation messages for context. Each item: { role: "user" | "assistant", content: string }

Returns

String — The AI assistant's response text.

Example

const { data: reply, errors } = await dataClient.queries.generateAiResponse({
  message: "What should I do after a car accident?"
});

if (errors) {
  console.error(errors);
} else {
  console.log(reply); // "First, make sure everyone is safe..."
}
const { data: reply } = await dataClient.queries.generateAiResponse({
  message: "What if the other driver was uninsured?",
  history: [
    { role: "user", content: "I was in a car accident" },
    { role: "assistant", content: "I'm sorry to hear that. Are you safe?" },
    { role: "user", content: "Yes, but my car is damaged" },
    { role: "assistant", content: "Here's what you should do next..." }
  ]
});

listCognitoUsers

Query listCognitoUsers

List all Cognito user pool users with their roles. Supports pagination. Admin-only.

Admin only

Arguments

NameTypeRequiredDescription
paginationTokenStringNoToken from previous response to fetch next page
limitIntNoMax users to return (1-60, default 50)

Returns — ListUsersResult

FieldTypeDescription
successBoolean!Whether the operation succeeded
messageString!Status message
users[CognitoUserItem]!Array of user objects
paginationTokenStringToken for next page (null if no more)

CognitoUserItem shape

FieldTypeDescription
userIdString!Cognito sub (UUID)
emailString!User email
firstNameStringgiven_name attribute
lastNameStringfamily_name attribute
phoneStringphone_number attribute
enabledBoolean!Account enabled flag
statusString!Cognito status (CONFIRMED, FORCE_CHANGE_PASSWORD, etc.)
roleString!Current Cognito group: Prospect | Client | Staff | Admin
createdAtStringISO datetime of account creation

Example

const { data: result } = await dataClient.queries.listCognitoUsers({
  limit: 20
});

if (result?.success) {
  result.users.forEach(user => {
    console.log(`${user.email} — ${user.role} — ${user.status}`);
  });

  if (result.paginationToken) {
    // Fetch next page
    const { data: page2 } = await dataClient.queries.listCognitoUsers({
      limit: 20,
      paginationToken: result.paginationToken
    });
  }
}

Filevine Resolvers (Lambda-backed)

All Filevine operations are backed by the filevine-integration Lambda and return the shared FilevineResult type. Client-callable operations are scope-checked server-side against the caller's ClientCaseLink records.

Shared return type — FilevineResult

FieldTypeDescription
successBoolean!Whether the operation succeeded
messageString!Status message
caseDataFilevineCaseDataSingle case (staffGetCase, clientGetCaseDetails)
cases[FilevineCaseData]Case list (staffListCases, clientGetMyCases)
matchedClientIdStringFilevine contact ID (matchAndLinkClient)
fvContactIdStringFilevine contact ID (matchAndLinkClient)
matters[FilevineCaseData]Matched client's cases (matchAndLinkClient)
notes[FilevineNote]Note list (staffListNotes, clientListMessages)
noteFilevineNoteCreated note (clientSendMessage)
noteIdStringCreated note ID (clientSendMessage)
okBooleanConnectivity flag (pingFilevine)
hasBearerBooleanBearer token acquired (pingFilevine)
hasOrgBooleanOrg ID resolved (pingFilevine)

FilevineCaseData shape

FieldType
externalCaseIdString!
caseTypeString!
statusString!
stageString!
attorneyString!
nextKeyDateString
notesString

FilevineNote shape

FieldType
noteIdString!
bodyString!
createdAtString!
authorNameString!
isPrivateBoolean

staffListCases

Query staffListCases

List Filevine projects (cases). Optionally filter to cases assigned to the calling staff member.

Staff Admin

Arguments

NameTypeRequiredDescription
assignedToMeBooleanNoOnly return cases assigned to the caller

Example

const { data } = await dataClient.queries.staffListCases({ assignedToMe: true });

if (data?.success) {
  data.cases?.forEach(c => console.log(`${c.externalCaseId}: ${c.stage}`));
}

staffGetCase

Query staffGetCase

Fetch a single Filevine project by its external case ID.

Staff Admin

Arguments

NameTypeRequiredDescription
externalCaseIdString!YesFilevine project ID

Example

const { data } = await dataClient.queries.staffGetCase({
  externalCaseId: "FV-2024-0042"
});

if (data?.success && data.caseData) {
  console.log(data.caseData.stage);    // "Negotiation"
  console.log(data.caseData.attorney); // "J. Roberts"
}

staffListNotes

Query staffListNotes

List notes on a Filevine project. Supports cursor pagination.

Staff Admin

Arguments

NameTypeRequiredDescription
externalCaseIdString!YesFilevine project ID
limitIntNoMax notes to return (default 50)
cursorStringNoPagination offset cursor

Example

const { data } = await dataClient.queries.staffListNotes({
  externalCaseId: "FV-2024-0042",
  limit: 25
});

data?.notes?.forEach(n => console.log(`${n.authorName}: ${n.body}`));

clientGetMyCases

Query clientGetMyCases

Return all Filevine cases linked to the calling user via ClientCaseLink records. Returns an empty list if the user has no linked cases.

authenticated

Arguments

None.

Example

const { data } = await dataClient.queries.clientGetMyCases({});

if (data?.success) {
  data.cases?.forEach(c => {
    console.log(`${c.caseType} — ${c.stage} — ${c.attorney}`);
  });
}

clientGetCaseDetails

Query clientGetCaseDetails

Fetch one of the caller's linked cases. The Lambda verifies the caller owns the case (via ClientCaseLink) before fetching — calls for unlinked cases fail with SCOPE_DENIED.

authenticated (scope-checked)

Arguments

NameTypeRequiredDescription
externalCaseIdString!YesFilevine project ID (must be linked to caller)

Example

const { data } = await dataClient.queries.clientGetCaseDetails({
  externalCaseId: "FV-2024-0042"
});

if (data?.success && data.caseData) {
  console.log(data.caseData.stage);
}

clientListMessages

Query clientListMessages

List messages (Filevine notes) for one of the caller's linked cases. Scope-checked. Supports cursor pagination.

authenticated (scope-checked)

Arguments

NameTypeRequiredDescription
externalCaseIdString!YesFilevine project ID (must be linked to caller)
limitIntNoMax messages to return (default 50)
cursorStringNoPagination offset cursor

Example

const { data } = await dataClient.queries.clientListMessages({
  externalCaseId: "FV-2024-0042",
  limit: 25
});

data?.notes?.forEach(n => console.log(`${n.authorName}: ${n.body}`));

clientSendMessage

Mutation clientSendMessage

Send a message to the firm on one of the caller's linked cases. Creates a Filevine note (prefixed with [authorLabel] when given). Scope-checked.

authenticated (scope-checked)

Arguments

NameTypeRequiredDescription
externalCaseIdString!YesFilevine project ID (must be linked to caller)
bodyString!YesMessage text
authorLabelStringNoDisplay name prefixed to the note body

Example

const { data } = await dataClient.mutations.clientSendMessage({
  externalCaseId: "FV-2024-0042",
  body: "When is my next appointment?",
  authorLabel: "Jane Doe"
});

if (data?.success) {
  console.log(`Note created: ${data.noteId}`);
}

matchAndLinkClient

Mutation matchAndLinkClient

Match a Cognito user to a Filevine contact by email, then create ClientCaseLink rows for each of the contact's projects. Also invoked automatically (best-effort) by the postConfirmation trigger after sign-up.

Admin only

Arguments

NameTypeRequiredDescription
targetUserIdString!YesCognito sub of the user to link
targetEmailString!YesEmail to match against Filevine contacts

Returns — ClientCaseLinkResult

FieldTypeDescription
successBoolean!Whether the match attempt succeeded
messageString!Status message
linkedCountIntNumber of ClientCaseLink rows created

Example

const { data } = await dataClient.mutations.matchAndLinkClient({
  targetUserId: "abc-123-def",
  targetEmail: "jane.doe@example.com"
});

if (data?.success) {
  console.log(`Linked ${data.linkedCount ?? 0} case(s)`);
}

pingFilevine

Query pingFilevine

Acceptance-test endpoint. Verifies Filevine credentials can be acquired (bearer token + org ID) without making data calls.

Admin only

Arguments

None.

Example

const { data } = await dataClient.queries.pingFilevine({});

console.log(data?.ok, data?.hasBearer, data?.hasOrg);

generateCaseDigest

Query generateCaseDigest

Generate an AI-powered plain-language digest of a client's active cases. Uses GPT-4o-mini to summarize case status, upcoming dates, and recommended next steps. Output is under 300 words.

authenticated Staff Admin

Arguments

NameTypeRequiredDescription
clientUserIdString!YesThe Cognito user ID of the client
caseSummaries[CaseDigestInput]!YesArray of case data to summarize

CaseDigestInput shape

FieldTypeRequired
externalCaseIdString!Yes
caseTypeString!Yes
statusString!Yes
stageString!Yes
attorneyString!Yes
nextKeyDateStringNo
notesStringNo

Returns — CaseDigestResult

FieldTypeDescription
successBoolean!Whether the digest was generated
digestString!The plain-language case summary

Example

const { data } = await dataClient.queries.generateCaseDigest({
  clientUserId: "abc-123-def",
  caseSummaries: [
    {
      externalCaseId: "FV-2024-0042",
      caseType: "Auto Accident",
      status: "active",
      stage: "Treatment",
      attorney: "J. Roberts",
      nextKeyDate: "2025-03-15",
      notes: "MRI scheduled for next week"
    }
  ]
});

if (data?.success) {
  console.log(data.digest);
  // "Your auto accident case is currently in the Treatment phase..."
}

Custom Mutations (Lambda-backed)

assignUserRole

Mutation assignUserRole

Change a user's Cognito group (role). Removes all existing role groups and adds the new one. Admin-only.

Admin only

Arguments

NameTypeRequiredDescription
targetUserIdString!YesCognito username (sub) of the user to update
targetRoleString!Yes New role. Must be one of:
Prospect Client Staff Admin

Returns — ManageRoleResult

FieldTypeDescription
successBoolean!Whether the role was updated
messageString!Status message
previousRoleStringThe user's previous role (null if none)
newRoleString!The role that was assigned

Example

const { data } = await dataClient.mutations.assignUserRole({
  targetUserId: "abc-123-def-456",
  targetRole: "Client"
});

if (data?.success) {
  console.log(`Changed from ${data.previousRole} to ${data.newRole}`);
  // "Changed from Prospect to Client"
}

inviteUser

Mutation inviteUser

Create a new Cognito user and send them an email invitation with a temporary password. The user is placed in the specified role group immediately. Admin-only.

Admin only

Arguments

NameTypeRequiredDescription
emailString!YesEmail address (becomes their username)
firstNameString!YesGiven name
lastNameString!YesFamily name
roleString!Yes Initial role:
Prospect Client Staff Admin
phoneStringNoPhone number (E.164 format, e.g. +15551234567)

Returns — InviteUserResult

FieldTypeDescription
successBoolean!Whether the invitation was sent
messageString!Status message
userIdStringNew user's Cognito sub (UUID)
emailString!The email the invite was sent to

Example

const { data } = await dataClient.mutations.inviteUser({
  email: "jane.doe@example.com",
  firstName: "Jane",
  lastName: "Doe",
  role: "Staff",
  phone: "+15551234567"
});

if (data?.success) {
  console.log(`Invited ${data.email}, user ID: ${data.userId}`);
}

submitToLeadDocket

Mutation submitToLeadDocket

Interact with the LeadDocket CRM. Two actions: submit a new lead from an intake form, or sync active matters for a client by email.

Staff Admin

Arguments

NameTypeRequiredDescription
actionString!Yes"submitIntake" or "syncMatters"
For action = "submitIntake":
contactNameStringFull name of the lead
contactEmailStringLead's email address
contactPhoneStringLead's phone number
incidentTypeStringType of incident (e.g. "Auto Accident")
incidentDateStringDate of incident (ISO format)
locationStringIncident location
injuriesStringDescription of injuries
medicalTreatmentStringMedical treatment received
insuranceInfoStringInsurance details
For action = "syncMatters":
emailStringClient email to look up matters for

Returns — LeadDocketResult

FieldTypeDescription
successBoolean!Whether the operation succeeded
messageString!Status message
externalLeadIdStringLeadDocket lead ID (submitIntake only)
matters[LeadDocketMatter]Active matters (syncMatters only)

LeadDocketMatter shape

FieldType
externalCaseIdString!
caseTypeString!
statusString!
clientNameString!

Examples

const { data } = await dataClient.mutations.submitToLeadDocket({
  action: "submitIntake",
  contactName: "John Smith",
  contactEmail: "john.smith@example.com",
  contactPhone: "+15559876543",
  incidentType: "Auto Accident",
  incidentDate: "2025-01-15",
  location: "Highway 61, Memphis TN",
  injuries: "Whiplash, back pain",
  medicalTreatment: "ER visit, follow-up with chiropractor"
});

if (data?.success) {
  console.log(`Lead created: ${data.externalLeadId}`);
}
const { data } = await dataClient.mutations.submitToLeadDocket({
  action: "syncMatters",
  email: "jane.doe@example.com"
});

if (data?.success) {
  console.log(`Found ${data.matters?.length ?? 0} matters`);
  data.matters?.forEach(m => {
    console.log(`${m.externalCaseId}: ${m.caseType} — ${m.status}`);
  });
}

Data Models (Auto-CRUD via AppSync)

Each model below gets automatic create, get, list, update, and delete operations through the Amplify Data client. All models also get id, createdAt, and updatedAt fields automatically.

CRUD pattern: dataClient.models.ModelName.create({ ... }), .get({ id }), .list(), .update({ id, ... }), .delete({ id })

UserProfile

Model UserProfile

Core profile for all authenticated users. The record id is the user's Cognito sub — each user can only read/write their own profile.

owner (id = caller's sub)
FieldTypeRequired
emailString!Yes
firstNameStringNo
lastNameStringNo
phoneNumberAWSPhoneNo
locationStringNo
occupationStringNo

Example

// Create (id should be the user's Cognito sub)
await dataClient.models.UserProfile.create({
  id: "cognito-sub-uuid",
  email: "user@example.com",
  firstName: "Jane",
  lastName: "Doe",
  phoneNumber: "+15551234567",
  location: "Memphis, TN"
});

// List all
const { data: profiles } = await dataClient.models.UserProfile.list();

// Get by ID
const { data: profile } = await dataClient.models.UserProfile.get({ id: "abc-123" });

ClientProfile

Model ClientProfile

Extended profile for clients with case details.

owner Staff Admin
FieldTypeRequiredDescription
ownerStringNoAuto-set to current user's identity
externalCaseIdStringNoFilevine/LeadDocket case ID
dateOfAccidentAWSDateNoYYYY-MM-DD
injuryTypeStringNoType of injury
primaryAttorneyStringNoAssigned attorney name
statusStringNoDefault: "intake"

Example

await dataClient.models.ClientProfile.create({
  externalCaseId: "FV-2024-0042",
  dateOfAccident: "2024-12-01",
  injuryType: "Whiplash",
  primaryAttorney: "J. Roberts"
});

StaffProfile

Model StaffProfile

Profile for firm staff members.

owner Admin
FieldTypeRequiredDescription
ownerStringNoAuto-set
positionStringNoJob title
isAdminBooleanNoDefault: false
firmLocationStringNoOffice location

Example

await dataClient.models.StaffProfile.create({
  position: "Paralegal",
  firmLocation: "Memphis Office"
});

NotificationPreferences

Model NotificationPreferences

Per-user notification settings.

owner only
FieldTypeDefault
caseUpdatesBooleantrue
docRequestsBooleantrue
blogUpdatesBooleantrue
lawUpdatesBooleantrue
recoveryRemindersBooleantrue

Example

// Create with custom preferences
await dataClient.models.NotificationPreferences.create({
  caseUpdates: true,
  blogUpdates: false,
  recoveryReminders: true
});

// Update
await dataClient.models.NotificationPreferences.update({
  id: "existing-id",
  blogUpdates: true
});

IntakeSubmission

Model IntakeSubmission

New lead/intake form submissions. Can be created publicly (via API key) or by authenticated users. Full CRUD for Staff/Admin.

Auto-sync to LeadDocket: every new row triggers the intake-leaddocket-sync Lambda (DynamoDB stream, INSERT only). It POSTs the lead to LeadDocket server-side — regardless of who created the row, so anonymous apiKey submissions from the /intake form and the chat CTA are covered with no extra calls — then stamps externalLeadId and leadSyncStatus back onto the row. Staff queues using observeQuery see new leads in real time.

publicApiKey (create only) Staff (full CRUD) Admin (full CRUD)
FieldTypeRequiredDescription
incidentTypeString!Yese.g. "Auto Accident", "Slip & Fall"
incidentDateAWSDateNoYYYY-MM-DD
locationStringNoWhere it happened
injuriesStringNoDescription of injuries
medicalTreatmentStringNoTreatment received
policeReportBooleanNoDefault: false
insuranceInfoStringNoInsurance details
contactNameString!YesFull name
contactEmailAWSEmail!YesEmail address
contactPhoneAWSPhoneNoPhone number
externalLeadIdStringNoLeadDocket lead ID, written by intake-leaddocket-sync after a successful POST
leadSyncStatusEnumNo LeadDocket sync state, set by intake-leaddocket-sync.
pending synced failed
statusEnumNo
new reviewing contacted converted closed

Example

await dataClient.models.IntakeSubmission.create({
  incidentType: "Auto Accident",
  incidentDate: "2025-01-15",
  contactName: "John Smith",
  contactEmail: "john@example.com",
  contactPhone: "+15551234567",
  location: "Memphis, TN",
  injuries: "Back pain, whiplash"
});

CallbackRequest

Model CallbackRequest

Request for a callback from the firm. Can be created publicly or by authenticated users.

publicApiKey (create only) authenticated (create only) Staff (full CRUD) Admin (full CRUD)
FieldTypeRequiredDescription
contactNameString!YesFull name
contactPhoneAWSPhone!YesPhone to call back
contactEmailAWSEmailNoEmail address
preferredTimeStringNoWhen they prefer the callback
reasonStringNoReason for calling
statusEnumNo
pending assigned completed cancelled
assignedStaffIdStringNoStaff member handling the callback
notesStringNoInternal notes

Example

await dataClient.models.CallbackRequest.create({
  contactName: "Sarah Johnson",
  contactPhone: "+15559876543",
  preferredTime: "Afternoon",
  reason: "Question about my case status"
});

CaseSummary

Model CaseSummary

Internal case tracking record synced from Filevine/LeadDocket. Owners can only read; Staff/Admin have full CRUD.

owner (read only) Staff (full CRUD) Admin (full CRUD)
FieldTypeRequiredDescription
externalCaseIdString!YesFilevine project ID
clientUserIdString!YesCognito sub of the client
caseTypeString!Yese.g. "Auto Accident"
statusEnumNo
active settled closed
stageEnumNo
Investigation Treatment Negotiation Litigation Settlement Appeal
attorneyStringNoAssigned attorney
nextKeyDateAWSDateNoNext important date
notesStringNoInternal notes
messagesVisibleBooleanNoShow messages tab to client. Default: false
documentsVisibleBooleanNoShow documents tab to client. Default: false
appointmentsVisibleBooleanNoShow appointments tab to client. Default: false

Example

// Staff creates a case summary
await dataClient.models.CaseSummary.create({
  externalCaseId: "FV-2024-0042",
  clientUserId: "abc-123-def",
  caseType: "Auto Accident",
  status: "active",
  stage: "Treatment",
  attorney: "J. Roberts",
  nextKeyDate: "2025-03-15",
  messagesVisible: true,
  documentsVisible: false
});

// Client reads their own cases
const { data: myCases } = await dataClient.models.CaseSummary.list();

CaseDigest (Model)

Model CaseDigest

Stored AI-generated case digests for display in the client portal.

owner (read only) Staff (full CRUD) Admin (full CRUD)
FieldTypeRequired
clientUserIdString!Yes
caseIdStringNo
summaryString!Yes
generatedAtAWSDateTime!Yes
Model ClientCaseLink

Links a Cognito Client user to one or more Filevine projects. Created automatically on email match during post-confirmation, or manually by an Admin via matchAndLinkClient. Owner is matched on clientUserId (sub claim) — clients can only read their own links.

owner via clientUserId (read only) Staff (full CRUD) Admin (full CRUD)
FieldTypeRequiredDescription
clientUserIdString!YesCognito sub of the client
externalCaseIdString!YesFilevine project ID
fvContactIdStringNoFilevine contact ID
matchedAtAWSDateTime!YesWhen the match was made
confirmedByStaffIdStringNoStaff member who confirmed the link
activeBooleanNoDefault: true

Example

// Staff manually links a client to a case
await dataClient.models.ClientCaseLink.create({
  clientUserId: "abc-123-def",
  externalCaseId: "FV-2024-0042",
  fvContactId: "fv-contact-789",
  matchedAt: new Date().toISOString(),
  confirmedByStaffId: "staff-sub-456"
});

// Client reads their own links
const { data: links } = await dataClient.models.ClientCaseLink.list();

ClientCaseMessage

Model ClientCaseMessage

Dual-write mirror for client ↔ firm communications. Outbound rows start as pending and are stamped synced + fvNoteId once the Filevine note write succeeds. Inbound rows from the Filevine poller arrive already synced. Owner is matched on clientUserId (sub claim).

owner via clientUserId (read, create) Staff (full CRUD) Admin (full CRUD)
FieldTypeRequiredDescription
clientUserIdString!YesCognito sub of the client
externalCaseIdString!YesFilevine project ID
directionEnumNo
inbound outbound
bodyString!YesMessage text
fvNoteIdStringNoFilevine note ID once synced
sentAtAWSDateTime!YesWhen the message was sent
syncStatusEnumNo
pending synced failed
attemptsIntNoSync attempt count. Default: 0

Example

// Client writes an outbound message mirror row
await dataClient.models.ClientCaseMessage.create({
  clientUserId: "abc-123-def",
  externalCaseId: "FV-2024-0042",
  direction: "outbound",
  body: "When is my next appointment?",
  sentAt: new Date().toISOString(),
  syncStatus: "pending"
});

StaffAuditLog

Model StaffAuditLog

Append-only record of every server-side Filevine mutation, written by the filevine-integration Lambda. Used for compliance auditing without exposing personal data (no note bodies, no PII).

Admin only
FieldTypeRequiredDescription
actorSubString!YesCognito sub of the actor
actionString!YesAction performed (e.g. createProjectNote)
externalCaseIdStringNoFilevine project ID
successBoolean!YesWhether the mutation succeeded
fvStatusIntNoFilevine HTTP status code
tsAWSDateTime!YesTimestamp of the action

Example

// Admin reviews recent Filevine mutations
const { data: logs } = await dataClient.models.StaffAuditLog.list();
logs.forEach(l => console.log(`${l.ts} ${l.actorSub} ${l.action} ok=${l.success}`));

AiConversation

Model AiConversation

Persisted AI chat conversations. Each conversation has many AiMessage records.

owner only
FieldTypeRequiredDescription
ownerStringNoAuto-set
topicStringNoConversation topic/title
messages[AiMessage]hasMany relationship

Example

// Create a new conversation
const { data: convo } = await dataClient.models.AiConversation.create({
  topic: "Car Accident Question"
});

// List user's conversations
const { data: convos } = await dataClient.models.AiConversation.list();

// Get with messages
const { data: convoWithMsgs } = await dataClient.models.AiConversation.get(
  { id: convo.id },
  { selectionSet: ['id', 'topic', 'messages.*'] }
);

AiMessage

Model AiMessage

Individual messages within an AI conversation.

owner only
FieldTypeRequiredDescription
conversationIdID!YesFK to AiConversation
roleEnumNo
user assistant
contentString!YesMessage text

Example

// Save a user message
await dataClient.models.AiMessage.create({
  conversationId: "convo-id-123",
  role: "user",
  content: "What should I do after a car accident?"
});

// Save the assistant reply
await dataClient.models.AiMessage.create({
  conversationId: "convo-id-123",
  role: "assistant",
  content: "First, make sure everyone is safe..."
});

Auth Triggers

postConfirmation

Trigger PostConfirmation_ConfirmSignUp

Automatically fires after a user confirms their email. Adds the user to the Prospect Cognito group. This is not called by the frontend — it happens server-side automatically.

It then performs a best-effort Filevine email match (via the matchAndLinkClient mutation): if the new user's email matches a Filevine contact, ClientCaseLink rows are created so the user sees their case data on first sign-in. Any failure here is logged and never blocks sign-up confirmation.

Frontend impact: After sign-up + confirmation, the user's JWT will include cognito:groups: ["Prospect"]. No frontend action needed.


Roles & Groups Reference

RoleCan Access
Prospect UserProfile (CRUD own), NotificationPreferences (CRUD own), AiConversation/AiMessage (CRUD own), generateAiResponse (query), IntakeSubmission (create via API key), CallbackRequest (create), clientGetMyCases / clientGetCaseDetails / clientListMessages / clientSendMessage (scope-checked — empty until linked)
Client Everything Prospect can do, plus: ClientProfile (read/update own), CaseSummary (read own), CaseDigest model (read own), ClientCaseLink (read own), ClientCaseMessage (read/create own), generateCaseDigest (query)
Staff All model CRUD (except owner-only fields of other users), submitToLeadDocket, staffListCases, staffGetCase, staffListNotes, generateCaseDigest
Admin Everything Staff can do, plus: assignUserRole, inviteUser, listCognitoUsers, matchAndLinkClient, pingFilevine, StaffAuditLog (read), StaffProfile CRUD for all users

Error Handling

All Amplify Data client calls return { data, errors }. Always check errors.

const { data, errors } = await dataClient.queries.generateAiResponse({
  message: "Hello"
});

if (errors) {
  // errors is an array of GraphQL errors
  errors.forEach(err => {
    console.error(err.message);
    // Common error types:
    // - "Unauthorized" — user lacks required group
    // - "Validation error" — missing required field
    // - "Network error" — offline or connectivity issue
  });
  return;
}

// data is safe to use here

Lambda-backed operations also return a success: boolean and message: string within the data payload. Always check both:

const { data, errors } = await dataClient.mutations.assignUserRole({
  targetUserId: "abc",
  targetRole: "Client"
});

// 1. GraphQL-level error (auth, network, schema)
if (errors) { /* handle */ }

// 2. Business-logic error (invalid role, user not found)
if (!data?.success) {
  console.error(data?.message);
}

Environment Variables & Secrets

These are backend-only (Lambda) env vars. Frontend developers do not need to set these — they are configured in the Amplify sandbox.

VariableUsed ByDescription
OPENAI_API_KEYai-assistant, case-digestOpenAI API key (secret)
OPENAI_MODELai-assistant, case-digestModel ID, default: gpt-4o-mini
LEADDOCKET_API_KEYleaddocket-integration, intake-leaddocket-syncLeadDocket API key (secret)
LEADDOCKET_API_URLleaddocket-integration, intake-leaddocket-syncLeadDocket API base URL (secret)
FILEVINE_PATfilevine-integration, filevine-inbound-syncFilevine Personal Access Token (secret)
FILEVINE_CLIENT_IDfilevine-integration, filevine-inbound-syncFilevine API client ID (secret)
FILEVINE_CLIENT_SECRETfilevine-integration, filevine-inbound-syncFilevine API client secret (secret)
FILEVINE_REGIONfilevine-integration, filevine-inbound-syncFilevine region — US or CA (static env)
AMPLIFY_AUTH_USERPOOL_IDmanage-rolesCognito User Pool ID (auto-injected)
AMPLIFY_DATA_GRAPHQL_ENDPOINTpost-confirmation, filevine-integration, filevine-inbound-sync, intake-leaddocket-syncAppSync endpoint for IAM-auth writes (auto-injected)

RM Pocket Lawyer Backend API Reference — Generated for Roberts Markland LLP

Last updated: