Openfront Law Firm

Firm-owned intake, conflicts, matters, documents, billing, trust, client portal, public site, and governance.

Source · Live site · Catalog

Openfront Law Firm is an open-source app for running a law practice. Staff handle intake, conflict checks, matters, dockets, deadlines, documents, messages, time, billing, retainers, and trust records in one dashboard. Visitors can read about the firm, browse practice areas and attorneys, open an account, and request a consultation. Clients can see items the firm shares with them in a portal.

To install it, see Getting started.

Concepts

ConceptMeaning
FirmThe tenant. Every firm-owned record carries a firm and cannot link to another firm's records
Membership and roleStaff access comes from an active FirmMembership and its Role (39 can* flags)
Matter teamMatterTeamMember rows with canEdit, canBill, canAccessRestricted
Ethical wallBlocks named users from a matter, even administrators
Controlled recordStatus, money, access, and evidence fields change only through named mutations with a serializable transaction and an AuditEvent
Minor unitsMoney is integer minor units plus a currency code
Trust ledgerClient or matter ledger inside a trust account, posted with balanced entries

Stack

Next.js 16 (App Router) with React 19, Keystone 6 with a GraphQL API, Prisma with PostgreSQL, Tailwind CSS 4, and shadcn/ui. Sign-in uses Keystone password auth with stateless session cookies.

Source layout

PathContents
features/keystone/models79 Keystone lists, registered in models/index.ts
features/keystone/mutationsCustom queries and mutations, registered in mutations/index.ts
features/keystone/access.ts, tenancy.ts, lib/Permissions, firm scoping, lifecycles, matter access, mail, webhooks
features/platformStaff pages under /dashboard/platform
features/legal-aiLegal AI screens (mounted through features/platform/legal-ai)
features/dashboardShared dashboard shell, sign-in, generic list pages
features/storefrontPublic firm site, customer account, and client portal screens
app/Thin route files
docs/schema-map.mdThe repository's own map of lists, workflows, and page coverage

schema.graphql and schema.prisma are generated from the models.

Tenancy and access

Firm is the tenant. Every firm-owned list carries a firm relationship. Shared hooks set the active firm on create and reject links to records from another firm. Lists with a lifecycle check each status change against a fixed transition table.

Staff access comes from an active FirmMembership and that firm's Role. A role has 39 can* flags, for example canManageIntake, canRunConflictChecks, canResolveConflicts, canManageMatters, canManageEthicalWalls, canManageBilling, canManageTrust, canManagePortal, canManageRetention, canManageIntegrations, canManageAgents, and canViewAudit.

Matter-scoped lists add three more checks:

  • team membership on the matter, with canEdit, canBill, and canAccessRestricted;
  • ethical walls, which can block a user from a matter;
  • authorizedUsers and blockedUsers on Matter, which are maintained by the team and wall mutations and cannot be written directly.

A firm administrator does not silently pass an ethical wall.

Data model

AreaLists
Firm and peopleFirm, Office, User, FirmMembership, Role
Machine accessApiKey, OAuthClient, OAuthConsent, OAuthToken, AgentPrincipal, AgentCredential, AgentGrant, AgentAction, AgentActionEvent
ContactsContact, Address, ContactMethod
IntakeIntake, IntakeParticipant, IntakeSubmission, Consultation
ConflictsConflictCheck, ConflictSearchTerm, ConflictMatch, ConflictDisposition
MattersPracticeArea, Matter, MatterParty, MatterTeamMember, EthicalWall, EthicalWallMember, MatterStatusEvent
CalendarCourt, Docket, DocketEntry, Deadline, LegalTask
CommunicationsCommunicationThread, CommunicationParticipant, CommunicationMessage, CommunicationDelivery
DocumentsDocument, DocumentVersion, SignatureRequest, DocumentSigner, SignatureEvent
BillingRateCard, RateRule, TimeEntry, Expense, RetainerAgreement, Invoice, InvoiceLine
PaymentsPayment, PaymentAllocation, Refund
TrustTrustAccount, TrustLedgerAccount, TrustTransaction, TrustLedgerEntry, TrustBalanceSnapshot, TrustReconciliation
Client portalPortalAccount, PortalMatterAccess, PortalShare, PortalSession, PortalDiscoveryPolicy, PortalGrantIntent
Public sitePublicSitePolicy, AttorneyProfile
IntegrationsIntegrationConnection, ExternalRecordLink, WebhookSubscription, WebhookDelivery, WebhookDeliveryAttempt
GovernanceRetentionPolicy, RetentionHold, RetentionAction, AuditEvent

Money is stored as integer minor units with a currency code. Document versions, trust entries, matter status events, signature events, audit events, and agent action events are append-only. Trust postings are corrected with reversing transactions, not edits.

Key lifecycles:

  • Intake: new → screening → conflict_review → consultation → decision → converted, declined, or withdrawn.
  • Conflict check: requested → running → review_required → cleared or conflict_found.
  • Matter: proposed → pending_conflict → open, on_hold, closing, closed; or declined.
  • Consultation: requested → scheduled → completed, cancelled, or no_show.
  • Invoice: draft → approved → issued → partially_paid → paid; also overdue and void.
  • Refund: requested → approved → processing → succeeded or failed.

Workflows

  1. Onboarding. completeFirmOnboarding creates the firm, first office, administrator role, four standard roles, your membership, and a default practice area. See Getting started.
  2. Publish the site. Publish a PublicSitePolicy (privacy notice, first-contact notice, about text, accounts and booking switches), mark practice areas public with a slug, fee, and length, and publish attorney profiles.
  3. Online booking. A visitor creates an account (registerConsultationAccount) and books a slot (bookClientConsultation). This creates a contact, an intake with a submission, a consultation in requested, and a conflict check when an opposing party is named. Slots are 9:00 to 17:00 on weekdays. Staff confirm or change it with scheduleConsultation and transitionConsultation. The customer can cancel with cancelOwnConsultation.
  4. Intake to matter. startClientIntake, addIntakeParticipant, conflict search terms and matches, recordConflictDisposition, finalizeConflictCheck, then convertIntakeToMatter, which creates the matter, parties, and team.
  5. Matter administration. setMatterTeamAccess, addMatterParty, changeMatterParty, transitionEthicalWall, setEthicalWallMember, changeMatterConfidentiality, transitionMatter.
  6. Courts and deadlines. Courts and dockets, recordDocketEntry, verifyDocketEntry, deadlines (reviseDraftDeadline, transitionDeadline), and tasks (transitionLegalTask).
  7. Documents. ingestDocumentVersion uploads to the S3 bucket, setDocumentCurrentVersion, then prepareSignatureRequest and addDocumentSigner.
  8. Messages. Threads, addCommunicationParticipant, a draft message, prepareCommunicationDeliveries (email only), then queueCommunicationMessage. The outbox worker sends email through SMTP.
  9. Time to cash. Rate cards and rules, time entries and expenses (transitionTimeEntry, transitionExpense), assembleDraftInvoice, transitionInvoice, recordManualPayment, allocatePayment, reversePaymentAllocation, requestRefund, reviewRefund. Retainers use prepareRetainerAgreement and transitionRetainerAgreement.
  10. Trust. Trust accounts and client or matter ledgers, postTrustTransaction (balanced, idempotent, serializable), and three-way reconciliation with prepareTrustReconciliation, reviseTrustReconciliation, transitionTrustReconciliation.
  11. Client portal. recordPortalDiscoveryPolicy, invitePortalAccount, acceptPortalInvitation, executePortalGrant, sharePortalResource (document, invoice, or thread), revokePortalGrant.
  12. People. inviteFirmMember, changeFirmMemberAccess, updateFirmRoleAuthority.
  13. Governance. transitionRetentionPolicy, assignDocumentRetentionPolicy, placeRetentionHold, releaseRetentionHold, disposeRetainedDocument.
  14. Data. previewContactImport and applyContactImport for contacts from CSV; preparePracticeExport for contacts, matters, invoices, time entries, or audit events (up to 500 rows).

Status, money, access indexes, credentials, and evidence fields are not writable through generic list updates on these lists. Changes go through the named mutations, which use a serializable transaction, an advisory lock, an idempotency key where money or access moves, and an audit event.

Named GraphQL operations

Custom queries in features/keystone/mutations/index.ts:

  • Public and customer: consultationCatalog, consultationCatalogPage, consultationService, consultationAvailability, customerConsultations, customerConsultationPage, customerConsultation, portalSharedResource.
  • Staff: practiceOverview, practiceWorkspace(category), previewContactImport, portalPolicyWorkspace.
  • Viewer checks: redirectToInit, viewerCanAccessDashboard, viewerCanRunConflictChecks, viewerCanResolveConflicts, viewerNeedsFirmOnboarding.

Custom mutations, grouped:

GroupMutations
Onboarding and peoplecompleteFirmOnboarding, inviteFirmMember, changeFirmMemberAccess, updateFirmRoleAuthority
CustomerregisterConsultationAccount, bookClientConsultation, cancelOwnConsultation
Intake and conflictsstartClientIntake, addIntakeParticipant, changeIntakeParticipant, transitionIntake, scheduleConsultation, transitionConsultation, requestConflictCheck, addConflictSearchTerm, addConflictMatch, transitionConflictCheck, recordConflictDisposition, finalizeConflictCheck, convertIntakeToMatter
Matters and calendartransitionMatter, changeMatterConfidentiality, setMatterTeamAccess, addMatterParty, changeMatterParty, transitionEthicalWall, setEthicalWallMember, recordDocketEntry, reviseDocketEntry, verifyDocketEntry, reviseDraftDeadline, transitionDeadline, transitionLegalTask
Documents and signaturesingestDocumentVersion, setDocumentCurrentVersion, transitionDocument, prepareSignatureRequest, addDocumentSigner, transitionDocumentSigner, transitionSignatureRequest
CommunicationsaddCommunicationParticipant, removeCommunicationParticipant, prepareCommunicationDeliveries, queueCommunicationMessage, transitionCommunicationDelivery
BillingtransitionTimeEntry, transitionExpense, assembleDraftInvoice, transitionInvoice, transitionPayment, recordManualPayment, allocatePayment, reversePaymentAllocation, requestRefund, reviewRefund, recordRefundProviderOutcome, prepareRetainerAgreement, transitionRetainerAgreement
TrustsetTrustAccountBankIdentity, transitionTrustAccount, transitionTrustLedgerAccount, postTrustTransaction, prepareTrustReconciliation, reviseTrustReconciliation, transitionTrustReconciliation
PortalrecordPortalDiscoveryPolicy, executePortalGrant, invitePortalAccount, acceptPortalInvitation, grantPortalMatterAccess, sharePortalResource, revokePortalGrant
RetentiontransitionRetentionPolicy, assignDocumentRetentionPolicy, placeRetentionHold, releaseRetentionHold, disposeRetainedDocument
Integrations and datarotateIntegrationCredential, transitionIntegrationConnection, enqueueWebhookDelivery, rotateWebhookSigningSecret, applyContactImport, preparePracticeExport
Machine access and agentsissueApiKey, revokeApiKey, issueOAuthClient, revokeOAuthClient, issueAgentCredential, revokeAgentCredential, transitionAgentPrincipal, updateAgentGrantAuthority, requestAgentAction, reviewAgentAction, executeAgentAction

The dashboard calls these through named documents in features/platform (for example CompleteFirmOnboarding, ConvertIntake, PostTrustTransaction, PracticeWorkspace).

GraphQL boundary

/api/graphql (in pages/api/graphql.ts) accepts POST only. It requires JSON, or multipart from the same origin for uploads. Bodies are capped at 1 MB and uploads at 10 MB and 5 files. Lists return at most 100 items per query. Query depth and fragment expansion are limited. Introspection and the playground are off, and internal error details are removed. Bearer tokens (API keys, OAuth access tokens, agent credentials) are accepted and checked against their scopes and the current owner's permissions.

Integrations

AreaWhat the code does
Document storageUploads go to the configured S3-compatible bucket. Downloads use signed links that expire after 5 minutes.
EmailPassword reset, portal sign-in, and portal invitation emails use SMTP. Message deliveries are sent by the outbox worker (features/keystone/webhooks/run.ts) through the same SMTP settings, with up to 5 attempts. Only email deliveries can be prepared; portal and SMS delivery are not configured.
WebhooksSubscriptions, signed deliveries (x-openfront-signature header), and attempts. The worker only contacts hosts listed in WEBHOOK_ALLOWED_HOSTS.
Provider connectionsIntegrationConnection records for email, calendar, document storage, signature, payment, court, accounting, or other. Credentials are stored encrypted with INTEGRATION_CREDENTIAL_KEY. A connection record stores the credential; it does not by itself add an adapter.
API keys and OAuthissueApiKey shows the secret once. OAuth uses /api/oauth/authorize and /api/oauth/token with authorization code plus S256 PKCE and refresh token rotation.
AgentsAgent principals, credentials, and grants scoped to one firm and optionally one matter. requestAgentAction → reviewAgentAction → executeAgentAction for a fixed set of action types (for example task.draft, matter.transition, invoice.issue, trust.post, communication.send). Agents are managed in the generic Models pages, not a platform page.
AI chatThe dashboard AI sidebar can use a shared OpenRouter key (OPENROUTER_*) or a key the user enters.
MCP/api/mcp-transport/http returns 501 LEGAL_MCP_DISABLED. Generic MCP tools for legal data are turned off.

More

On this page