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
| Concept | Meaning |
|---|---|
| Firm | The tenant. Every firm-owned record carries a firm and cannot link to another firm's records |
| Membership and role | Staff access comes from an active FirmMembership and its Role (39 can* flags) |
| Matter team | MatterTeamMember rows with canEdit, canBill, canAccessRestricted |
| Ethical wall | Blocks named users from a matter, even administrators |
| Controlled record | Status, money, access, and evidence fields change only through named mutations with a serializable transaction and an AuditEvent |
| Minor units | Money is integer minor units plus a currency code |
| Trust ledger | Client 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
| Path | Contents |
|---|---|
features/keystone/models | 79 Keystone lists, registered in models/index.ts |
features/keystone/mutations | Custom queries and mutations, registered in mutations/index.ts |
features/keystone/access.ts, tenancy.ts, lib/ | Permissions, firm scoping, lifecycles, matter access, mail, webhooks |
features/platform | Staff pages under /dashboard/platform |
features/legal-ai | Legal AI screens (mounted through features/platform/legal-ai) |
features/dashboard | Shared dashboard shell, sign-in, generic list pages |
features/storefront | Public firm site, customer account, and client portal screens |
app/ | Thin route files |
docs/schema-map.md | The 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, andcanAccessRestricted; - ethical walls, which can block a user from a matter;
authorizedUsersandblockedUsersonMatter, 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
| Area | Lists |
|---|---|
| Firm and people | Firm, Office, User, FirmMembership, Role |
| Machine access | ApiKey, OAuthClient, OAuthConsent, OAuthToken, AgentPrincipal, AgentCredential, AgentGrant, AgentAction, AgentActionEvent |
| Contacts | Contact, Address, ContactMethod |
| Intake | Intake, IntakeParticipant, IntakeSubmission, Consultation |
| Conflicts | ConflictCheck, ConflictSearchTerm, ConflictMatch, ConflictDisposition |
| Matters | PracticeArea, Matter, MatterParty, MatterTeamMember, EthicalWall, EthicalWallMember, MatterStatusEvent |
| Calendar | Court, Docket, DocketEntry, Deadline, LegalTask |
| Communications | CommunicationThread, CommunicationParticipant, CommunicationMessage, CommunicationDelivery |
| Documents | Document, DocumentVersion, SignatureRequest, DocumentSigner, SignatureEvent |
| Billing | RateCard, RateRule, TimeEntry, Expense, RetainerAgreement, Invoice, InvoiceLine |
| Payments | Payment, PaymentAllocation, Refund |
| Trust | TrustAccount, TrustLedgerAccount, TrustTransaction, TrustLedgerEntry, TrustBalanceSnapshot, TrustReconciliation |
| Client portal | PortalAccount, PortalMatterAccess, PortalShare, PortalSession, PortalDiscoveryPolicy, PortalGrantIntent |
| Public site | PublicSitePolicy, AttorneyProfile |
| Integrations | IntegrationConnection, ExternalRecordLink, WebhookSubscription, WebhookDelivery, WebhookDeliveryAttempt |
| Governance | RetentionPolicy, 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
- Onboarding.
completeFirmOnboardingcreates the firm, first office, administrator role, four standard roles, your membership, and a default practice area. See Getting started. - 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. - Online booking. A visitor creates an account (
registerConsultationAccount) and books a slot (bookClientConsultation). This creates a contact, an intake with a submission, a consultation inrequested, and a conflict check when an opposing party is named. Slots are 9:00 to 17:00 on weekdays. Staff confirm or change it withscheduleConsultationandtransitionConsultation. The customer can cancel withcancelOwnConsultation. - Intake to matter.
startClientIntake,addIntakeParticipant, conflict search terms and matches,recordConflictDisposition,finalizeConflictCheck, thenconvertIntakeToMatter, which creates the matter, parties, and team. - Matter administration.
setMatterTeamAccess,addMatterParty,changeMatterParty,transitionEthicalWall,setEthicalWallMember,changeMatterConfidentiality,transitionMatter. - Courts and deadlines. Courts and dockets,
recordDocketEntry,verifyDocketEntry, deadlines (reviseDraftDeadline,transitionDeadline), and tasks (transitionLegalTask). - Documents.
ingestDocumentVersionuploads to the S3 bucket,setDocumentCurrentVersion, thenprepareSignatureRequestandaddDocumentSigner. - Messages. Threads,
addCommunicationParticipant, a draft message,prepareCommunicationDeliveries(email only), thenqueueCommunicationMessage. The outbox worker sends email through SMTP. - Time to cash. Rate cards and rules, time entries and expenses (
transitionTimeEntry,transitionExpense),assembleDraftInvoice,transitionInvoice,recordManualPayment,allocatePayment,reversePaymentAllocation,requestRefund,reviewRefund. Retainers useprepareRetainerAgreementandtransitionRetainerAgreement. - Trust. Trust accounts and client or matter ledgers,
postTrustTransaction(balanced, idempotent, serializable), and three-way reconciliation withprepareTrustReconciliation,reviseTrustReconciliation,transitionTrustReconciliation. - Client portal.
recordPortalDiscoveryPolicy,invitePortalAccount,acceptPortalInvitation,executePortalGrant,sharePortalResource(document, invoice, or thread),revokePortalGrant. - People.
inviteFirmMember,changeFirmMemberAccess,updateFirmRoleAuthority. - Governance.
transitionRetentionPolicy,assignDocumentRetentionPolicy,placeRetentionHold,releaseRetentionHold,disposeRetainedDocument. - Data.
previewContactImportandapplyContactImportfor contacts from CSV;preparePracticeExportfor 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:
| Group | Mutations |
|---|---|
| Onboarding and people | completeFirmOnboarding, inviteFirmMember, changeFirmMemberAccess, updateFirmRoleAuthority |
| Customer | registerConsultationAccount, bookClientConsultation, cancelOwnConsultation |
| Intake and conflicts | startClientIntake, addIntakeParticipant, changeIntakeParticipant, transitionIntake, scheduleConsultation, transitionConsultation, requestConflictCheck, addConflictSearchTerm, addConflictMatch, transitionConflictCheck, recordConflictDisposition, finalizeConflictCheck, convertIntakeToMatter |
| Matters and calendar | transitionMatter, changeMatterConfidentiality, setMatterTeamAccess, addMatterParty, changeMatterParty, transitionEthicalWall, setEthicalWallMember, recordDocketEntry, reviseDocketEntry, verifyDocketEntry, reviseDraftDeadline, transitionDeadline, transitionLegalTask |
| Documents and signatures | ingestDocumentVersion, setDocumentCurrentVersion, transitionDocument, prepareSignatureRequest, addDocumentSigner, transitionDocumentSigner, transitionSignatureRequest |
| Communications | addCommunicationParticipant, removeCommunicationParticipant, prepareCommunicationDeliveries, queueCommunicationMessage, transitionCommunicationDelivery |
| Billing | transitionTimeEntry, transitionExpense, assembleDraftInvoice, transitionInvoice, transitionPayment, recordManualPayment, allocatePayment, reversePaymentAllocation, requestRefund, reviewRefund, recordRefundProviderOutcome, prepareRetainerAgreement, transitionRetainerAgreement |
| Trust | setTrustAccountBankIdentity, transitionTrustAccount, transitionTrustLedgerAccount, postTrustTransaction, prepareTrustReconciliation, reviseTrustReconciliation, transitionTrustReconciliation |
| Portal | recordPortalDiscoveryPolicy, executePortalGrant, invitePortalAccount, acceptPortalInvitation, grantPortalMatterAccess, sharePortalResource, revokePortalGrant |
| Retention | transitionRetentionPolicy, assignDocumentRetentionPolicy, placeRetentionHold, releaseRetentionHold, disposeRetainedDocument |
| Integrations and data | rotateIntegrationCredential, transitionIntegrationConnection, enqueueWebhookDelivery, rotateWebhookSigningSecret, applyContactImport, preparePracticeExport |
| Machine access and agents | issueApiKey, 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
| Area | What the code does |
|---|---|
| Document storage | Uploads go to the configured S3-compatible bucket. Downloads use signed links that expire after 5 minutes. |
Password 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. | |
| Webhooks | Subscriptions, signed deliveries (x-openfront-signature header), and attempts. The worker only contacts hosts listed in WEBHOOK_ALLOWED_HOSTS. |
| Provider connections | IntegrationConnection 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 OAuth | issueApiKey shows the secret once. OAuth uses /api/oauth/authorize and /api/oauth/token with authorization code plus S256 PKCE and refresh token rotation. |
| Agents | Agent 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 chat | The 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. |