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.
| Stack | Details |
|---|---|
| API Layer | AWS AppSync (GraphQL) |
| Auth | Amazon Cognito User Pools (email login, optional TOTP MFA) |
| Database | DynamoDB (auto-managed by Amplify Data models) |
| Functions | AWS Lambda (Node.js 18, TypeScript) |
| Frontend | React Native (Expo 54) + Expo Router |
| Client SDK | aws-amplify v6 — generateClient<Schema>() |
Authentication
The default authorization mode is Cognito User Pool (JWT). Some public endpoints also accept an API Key for unauthenticated access.
User Groups (Roles)
| Group | Description | Assigned By |
|---|---|---|
Prospect | Default role on sign-up. Can use AI chat and submit intakes. | Auto (post-confirmation trigger) |
Client | Active legal client. Can view case summaries and documents. | Admin via assignUserRole |
Staff | Firm employee. Full CRUD on cases, intakes, callbacks. | Admin via assignUserRole |
Admin | Superuser. Can manage roles, invite users, and access all data. | Admin via assignUserRole |
User Attributes (Cognito)
| Attribute | Required | Mutable |
|---|---|---|
email | Yes (login) | No |
given_name | Yes | Yes |
family_name | Yes | Yes |
phone_number | No | Yes |
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"
});
authenticated() will throw an UnauthorizedException.
Custom Queries (Lambda-backed)
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).
Arguments
| Name | Type | Required | Description |
|---|---|---|---|
message | String | Yes | The 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
List all Cognito user pool users with their roles. Supports pagination. Admin-only.
Arguments
| Name | Type | Required | Description |
|---|---|---|---|
paginationToken | String | No | Token from previous response to fetch next page |
limit | Int | No | Max users to return (1-60, default 50) |
Returns — ListUsersResult
| Field | Type | Description |
|---|---|---|
success | Boolean! | Whether the operation succeeded |
message | String! | Status message |
users | [CognitoUserItem]! | Array of user objects |
paginationToken | String | Token for next page (null if no more) |
CognitoUserItem shape
| Field | Type | Description |
|---|---|---|
userId | String! | Cognito sub (UUID) |
email | String! | User email |
firstName | String | given_name attribute |
lastName | String | family_name attribute |
phone | String | phone_number attribute |
enabled | Boolean! | Account enabled flag |
status | String! | Cognito status (CONFIRMED, FORCE_CHANGE_PASSWORD, etc.) |
role | String! | Current Cognito group: Prospect | Client | Staff | Admin |
createdAt | String | ISO 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
| Field | Type | Description |
|---|---|---|
success | Boolean! | Whether the operation succeeded |
message | String! | Status message |
caseData | FilevineCaseData | Single case (staffGetCase, clientGetCaseDetails) |
cases | [FilevineCaseData] | Case list (staffListCases, clientGetMyCases) |
matchedClientId | String | Filevine contact ID (matchAndLinkClient) |
fvContactId | String | Filevine contact ID (matchAndLinkClient) |
matters | [FilevineCaseData] | Matched client's cases (matchAndLinkClient) |
notes | [FilevineNote] | Note list (staffListNotes, clientListMessages) |
note | FilevineNote | Created note (clientSendMessage) |
noteId | String | Created note ID (clientSendMessage) |
ok | Boolean | Connectivity flag (pingFilevine) |
hasBearer | Boolean | Bearer token acquired (pingFilevine) |
hasOrg | Boolean | Org ID resolved (pingFilevine) |
FilevineCaseData shape
| Field | Type |
|---|---|
externalCaseId | String! |
caseType | String! |
status | String! |
stage | String! |
attorney | String! |
nextKeyDate | String |
notes | String |
FilevineNote shape
| Field | Type |
|---|---|
noteId | String! |
body | String! |
createdAt | String! |
authorName | String! |
isPrivate | Boolean |
staffListCases
List Filevine projects (cases). Optionally filter to cases assigned to the calling staff member.
Arguments
| Name | Type | Required | Description |
|---|---|---|---|
assignedToMe | Boolean | No | Only 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
Fetch a single Filevine project by its external case ID.
Arguments
| Name | Type | Required | Description |
|---|---|---|---|
externalCaseId | String! | Yes | Filevine 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
List notes on a Filevine project. Supports cursor pagination.
Arguments
| Name | Type | Required | Description |
|---|---|---|---|
externalCaseId | String! | Yes | Filevine project ID |
limit | Int | No | Max notes to return (default 50) |
cursor | String | No | Pagination 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
Return all Filevine cases linked to the calling user via ClientCaseLink records. Returns an empty list if the user has no linked cases.
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
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.
Arguments
| Name | Type | Required | Description |
|---|---|---|---|
externalCaseId | String! | Yes | Filevine 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
List messages (Filevine notes) for one of the caller's linked cases. Scope-checked. Supports cursor pagination.
Arguments
| Name | Type | Required | Description |
|---|---|---|---|
externalCaseId | String! | Yes | Filevine project ID (must be linked to caller) |
limit | Int | No | Max messages to return (default 50) |
cursor | String | No | Pagination 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
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.
Arguments
| Name | Type | Required | Description |
|---|---|---|---|
externalCaseId | String! | Yes | Filevine project ID (must be linked to caller) |
body | String! | Yes | Message text |
authorLabel | String | No | Display 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
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.
Arguments
| Name | Type | Required | Description |
|---|---|---|---|
targetUserId | String! | Yes | Cognito sub of the user to link |
targetEmail | String! | Yes | Email to match against Filevine contacts |
Returns — ClientCaseLinkResult
| Field | Type | Description |
|---|---|---|
success | Boolean! | Whether the match attempt succeeded |
message | String! | Status message |
linkedCount | Int | Number 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
Acceptance-test endpoint. Verifies Filevine credentials can be acquired (bearer token + org ID) without making data calls.
Arguments
None.
Example
const { data } = await dataClient.queries.pingFilevine({});
console.log(data?.ok, data?.hasBearer, data?.hasOrg);
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.
Arguments
| Name | Type | Required | Description |
|---|---|---|---|
clientUserId | String! | Yes | The Cognito user ID of the client |
caseSummaries | [CaseDigestInput]! | Yes | Array of case data to summarize |
CaseDigestInput shape
| Field | Type | Required |
|---|---|---|
externalCaseId | String! | Yes |
caseType | String! | Yes |
status | String! | Yes |
stage | String! | Yes |
attorney | String! | Yes |
nextKeyDate | String | No |
notes | String | No |
Returns — CaseDigestResult
| Field | Type | Description |
|---|---|---|
success | Boolean! | Whether the digest was generated |
digest | String! | 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
Change a user's Cognito group (role). Removes all existing role groups and adds the new one. Admin-only.
Arguments
| Name | Type | Required | Description |
|---|---|---|---|
targetUserId | String! | Yes | Cognito username (sub) of the user to update |
targetRole | String! | Yes | New role. Must be one of:
Prospect
Client
Staff
Admin
|
Returns — ManageRoleResult
| Field | Type | Description |
|---|---|---|
success | Boolean! | Whether the role was updated |
message | String! | Status message |
previousRole | String | The user's previous role (null if none) |
newRole | String! | 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
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.
Arguments
| Name | Type | Required | Description |
|---|---|---|---|
email | String! | Yes | Email address (becomes their username) |
firstName | String! | Yes | Given name |
lastName | String! | Yes | Family name |
role | String! | Yes | Initial role:
Prospect
Client
Staff
Admin
|
phone | String | No | Phone number (E.164 format, e.g. +15551234567) |
Returns — InviteUserResult
| Field | Type | Description |
|---|---|---|
success | Boolean! | Whether the invitation was sent |
message | String! | Status message |
userId | String | New user's Cognito sub (UUID) |
email | String! | 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
Interact with the LeadDocket CRM. Two actions: submit a new lead from an intake form, or sync active matters for a client by email.
Arguments
| Name | Type | Required | Description |
|---|---|---|---|
action | String! | Yes | "submitIntake" or "syncMatters" |
| For action = "submitIntake": | |||
contactName | String | — | Full name of the lead |
contactEmail | String | — | Lead's email address |
contactPhone | String | — | Lead's phone number |
incidentType | String | — | Type of incident (e.g. "Auto Accident") |
incidentDate | String | — | Date of incident (ISO format) |
location | String | — | Incident location |
injuries | String | — | Description of injuries |
medicalTreatment | String | — | Medical treatment received |
insuranceInfo | String | — | Insurance details |
| For action = "syncMatters": | |||
email | String | — | Client email to look up matters for |
Returns — LeadDocketResult
| Field | Type | Description |
|---|---|---|
success | Boolean! | Whether the operation succeeded |
message | String! | Status message |
externalLeadId | String | LeadDocket lead ID (submitIntake only) |
matters | [LeadDocketMatter] | Active matters (syncMatters only) |
LeadDocketMatter shape
| Field | Type |
|---|---|
externalCaseId | String! |
caseType | String! |
status | String! |
clientName | String! |
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.
dataClient.models.ModelName.create({ ... }),
.get({ id }),
.list(),
.update({ id, ... }),
.delete({ id })
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.
| Field | Type | Required |
|---|---|---|
email | String! | Yes |
firstName | String | No |
lastName | String | No |
phoneNumber | AWSPhone | No |
location | String | No |
occupation | String | No |
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
Extended profile for clients with case details.
| Field | Type | Required | Description |
|---|---|---|---|
owner | String | No | Auto-set to current user's identity |
externalCaseId | String | No | Filevine/LeadDocket case ID |
dateOfAccident | AWSDate | No | YYYY-MM-DD |
injuryType | String | No | Type of injury |
primaryAttorney | String | No | Assigned attorney name |
status | String | No | Default: "intake" |
Example
await dataClient.models.ClientProfile.create({
externalCaseId: "FV-2024-0042",
dateOfAccident: "2024-12-01",
injuryType: "Whiplash",
primaryAttorney: "J. Roberts"
});
StaffProfile
Profile for firm staff members.
| Field | Type | Required | Description |
|---|---|---|---|
owner | String | No | Auto-set |
position | String | No | Job title |
isAdmin | Boolean | No | Default: false |
firmLocation | String | No | Office location |
Example
await dataClient.models.StaffProfile.create({
position: "Paralegal",
firmLocation: "Memphis Office"
});
NotificationPreferences
Per-user notification settings.
| Field | Type | Default |
|---|---|---|
caseUpdates | Boolean | true |
docRequests | Boolean | true |
blogUpdates | Boolean | true |
lawUpdates | Boolean | true |
recoveryReminders | Boolean | true |
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
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.
| Field | Type | Required | Description |
|---|---|---|---|
incidentType | String! | Yes | e.g. "Auto Accident", "Slip & Fall" |
incidentDate | AWSDate | No | YYYY-MM-DD |
location | String | No | Where it happened |
injuries | String | No | Description of injuries |
medicalTreatment | String | No | Treatment received |
policeReport | Boolean | No | Default: false |
insuranceInfo | String | No | Insurance details |
contactName | String! | Yes | Full name |
contactEmail | AWSEmail! | Yes | Email address |
contactPhone | AWSPhone | No | Phone number |
externalLeadId | String | No | LeadDocket lead ID, written by intake-leaddocket-sync after a successful POST |
leadSyncStatus | Enum | No |
LeadDocket sync state, set by intake-leaddocket-sync.
pending
synced
failed
|
status | Enum | No |
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
Request for a callback from the firm. Can be created publicly or by authenticated users.
| Field | Type | Required | Description |
|---|---|---|---|
contactName | String! | Yes | Full name |
contactPhone | AWSPhone! | Yes | Phone to call back |
contactEmail | AWSEmail | No | Email address |
preferredTime | String | No | When they prefer the callback |
reason | String | No | Reason for calling |
status | Enum | No |
pending
assigned
completed
cancelled
|
assignedStaffId | String | No | Staff member handling the callback |
notes | String | No | Internal notes |
Example
await dataClient.models.CallbackRequest.create({
contactName: "Sarah Johnson",
contactPhone: "+15559876543",
preferredTime: "Afternoon",
reason: "Question about my case status"
});
CaseSummary
Internal case tracking record synced from Filevine/LeadDocket. Owners can only read; Staff/Admin have full CRUD.
| Field | Type | Required | Description |
|---|---|---|---|
externalCaseId | String! | Yes | Filevine project ID |
clientUserId | String! | Yes | Cognito sub of the client |
caseType | String! | Yes | e.g. "Auto Accident" |
status | Enum | No |
active
settled
closed
|
stage | Enum | No |
Investigation
Treatment
Negotiation
Litigation
Settlement
Appeal
|
attorney | String | No | Assigned attorney |
nextKeyDate | AWSDate | No | Next important date |
notes | String | No | Internal notes |
messagesVisible | Boolean | No | Show messages tab to client. Default: false |
documentsVisible | Boolean | No | Show documents tab to client. Default: false |
appointmentsVisible | Boolean | No | Show 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)
Stored AI-generated case digests for display in the client portal.
| Field | Type | Required |
|---|---|---|
clientUserId | String! | Yes |
caseId | String | No |
summary | String! | Yes |
generatedAt | AWSDateTime! | Yes |
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.
| Field | Type | Required | Description |
|---|---|---|---|
clientUserId | String! | Yes | Cognito sub of the client |
externalCaseId | String! | Yes | Filevine project ID |
fvContactId | String | No | Filevine contact ID |
matchedAt | AWSDateTime! | Yes | When the match was made |
confirmedByStaffId | String | No | Staff member who confirmed the link |
active | Boolean | No | Default: 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
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).
| Field | Type | Required | Description |
|---|---|---|---|
clientUserId | String! | Yes | Cognito sub of the client |
externalCaseId | String! | Yes | Filevine project ID |
direction | Enum | No |
inbound
outbound
|
body | String! | Yes | Message text |
fvNoteId | String | No | Filevine note ID once synced |
sentAt | AWSDateTime! | Yes | When the message was sent |
syncStatus | Enum | No |
pending
synced
failed
|
attempts | Int | No | Sync 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
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).
| Field | Type | Required | Description |
|---|---|---|---|
actorSub | String! | Yes | Cognito sub of the actor |
action | String! | Yes | Action performed (e.g. createProjectNote) |
externalCaseId | String | No | Filevine project ID |
success | Boolean! | Yes | Whether the mutation succeeded |
fvStatus | Int | No | Filevine HTTP status code |
ts | AWSDateTime! | Yes | Timestamp 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
Persisted AI chat conversations. Each conversation has many AiMessage records.
| Field | Type | Required | Description |
|---|---|---|---|
owner | String | No | Auto-set |
topic | String | No | Conversation 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
Individual messages within an AI conversation.
| Field | Type | Required | Description |
|---|---|---|---|
conversationId | ID! | Yes | FK to AiConversation |
role | Enum | No |
user
assistant
|
content | String! | Yes | Message 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
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
| Role | Can 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.
| Variable | Used By | Description |
|---|---|---|
OPENAI_API_KEY | ai-assistant, case-digest | OpenAI API key (secret) |
OPENAI_MODEL | ai-assistant, case-digest | Model ID, default: gpt-4o-mini |
LEADDOCKET_API_KEY | leaddocket-integration, intake-leaddocket-sync | LeadDocket API key (secret) |
LEADDOCKET_API_URL | leaddocket-integration, intake-leaddocket-sync | LeadDocket API base URL (secret) |
FILEVINE_PAT | filevine-integration, filevine-inbound-sync | Filevine Personal Access Token (secret) |
FILEVINE_CLIENT_ID | filevine-integration, filevine-inbound-sync | Filevine API client ID (secret) |
FILEVINE_CLIENT_SECRET | filevine-integration, filevine-inbound-sync | Filevine API client secret (secret) |
FILEVINE_REGION | filevine-integration, filevine-inbound-sync | Filevine region — US or CA (static env) |
AMPLIFY_AUTH_USERPOOL_ID | manage-roles | Cognito User Pool ID (auto-injected) |
AMPLIFY_DATA_GRAPHQL_ENDPOINT | post-confirmation, filevine-integration, filevine-inbound-sync, intake-leaddocket-sync | AppSync endpoint for IAM-auth writes (auto-injected) |
RM Pocket Lawyer Backend API Reference — Generated for Roberts Markland LLP
Last updated: