Leganews Pro
A search and monitoring platform for Congolese legal texts

Leganews began in 2019 with a practical problem. Legal professionals in the Democratic Republic of Congo often search official publications, institutional websites, scanned files, private archives, and documents shared by colleagues before they find the text they need.
I co-founded the product and worked on both its direction and its software. That meant listening to legal professionals and editors, maintaining the first production application, and deciding which parts of its model could no longer support the next version.
Leganews evolved from a legal database into a legal monitoring platform built around a curated corpus, structured search, versioned legal texts, editorial review, legal relationships, alerts, document comparison, access entitlements, and billing.
This was not a clean rewrite. The first product proved the need for search, subscriptions, payments, favorites, history, and editorial tools. It also showed where generic content models and coupled responsibilities made each new feature harder to add. The current platform carries those lessons into a more specific legal domain model.
The goal is not to replace official publications or the judgment of a legal professional. The goal is to reduce the time and uncertainty between a legal question and the sources required to work on it responsibly.
One rule guides the product:
A legal text becomes truly useful when it can be found, verified, read in the correct version, connected to related texts, and monitored as it changes.
The problem
Leganews addresses the uncertainty created when legal information is split across many sources.
In the Democratic Republic of Congo, a lawyer, company, researcher, magistrate, or student may need to move between the Journal officiel, institutional websites, search engines, PDF collections, physical archives, and documents received from colleagues before locating a useful text.
Even after finding a document, several questions remain:
- Is this the correct legal text?
- Is the title, number, date, and source accurate?
- Is this the original version or a later version?
- Has it been modified, completed, replaced, or repealed?
- Is the document complete?
- Is there a related decision or implementing text?
- How can I find it again when I return to the same matter?
- How will I know when something relevant changes?
A folder of PDFs solves storage, but it does not solve legal retrieval. A generic website search can find keywords, but it does not understand legal types, jurisdictions, classifications, subjects, versions, or relations. A simple database can list records, but it does not automatically provide an editorial process for deciding what becomes public.
Legal research becomes repeated reconstruction work. Each professional builds a personal system of bookmarks, folders, filenames, notes, and memory. Colleagues cannot easily verify, share, or maintain that system.
Leganews replaces that fragmented workflow with this path:
The corpus is intentionally described as curated rather than exhaustive. The presence of a text in Leganews does not, by itself, prove that the text is currently in force. The platform helps users inspect sources, dates, versions, and relations, but the final legal analysis remains a professional responsibility.
Product direction
Leganews started with a direct product idea:
Build a structured digital database of Congolese legal texts.
That idea was useful, but it was not sufficient.
A professional legal platform had to do more than upload and display documents. It needed to:
- model normative acts and case law without flattening both into generic files;
- preserve the stable identity of a legal text while supporting several versions;
- structure long texts into articles, sections, chapters, paragraphs, and annexes;
- separate draft editorial work from public publication;
- connect texts through legally meaningful relations;
- provide full-text search with legal filters;
- let users save texts in contextual folders and attach personal notes;
- monitor domains, subjects, and individual texts;
- notify users when a matching version is published;
- compare two versions or uploaded documents;
- support individual and organization access;
- process local payment methods safely;
- migrate years of legacy data without losing business meaning;
- remain observable and testable as the product grows.
This changed the product from a content website into a legal information system.
The public application supports research and monitoring. The administration application supports editorial and operational work. The Symfony API owns the business rules, while PostgreSQL, Redis, object storage, and Mercure handle search, background work, files, and realtime updates.
From the legacy platform to Leganews 2.0
The first Leganews platform was not a throwaway prototype. It was a production Symfony application that already followed Domain-Driven Design, processed subscriptions and FlexPay transactions, managed users, published legal content, stored favorites and search history, generated PDFs, and provided public and administrative interfaces.
Its stack reflected the product at that stage:
The first application grouped code into ContentManagement, IdentityAndAccess, SubscriptionBilling, Marketing, and a SharedKernel. That model helped us ship, but its concepts became too broad for the next version.
The legacy Content entity is a good example. One object contained the title, Markdown, HTML, excerpt, category, subcategory, publication date, online state, public-access flag, source, type, legal state, reference, favorite count, view count, and unique-view count.
That model was efficient for publishing one current article-like record. It became limiting when we needed to express a stable legal object with several versions, structured articles and chapters, editorial transitions, consolidation, citations, attachments, generated manifestations, and different access rules.
The same pressure appeared elsewhere:
Favoritecould only connect one user to one content record, but legal research needed named folders, saved items, notes, and context.SubscriptionBillingowned both commercial payment behavior and the access created by a purchase, making payment and entitlement rules harder to evolve independently.IdentityAndAccesscombined account identity with organization and subscription access concerns.- direct object references crossed context boundaries, so loading or changing one concept could pull another context's model into the same operation;
- the public website and administration UI were delivered by the same Symfony, Twig, and Symfony UX runtime even though their workflows were becoming very different;
- MariaDB stored the product well, but the next search and analytics requirements benefited from PostgreSQL full-text search, trigrams, range-friendly data, JSONB, and HyperLogLog.
The refactor was therefore not a framework upgrade. It was a change in the way we described the business.
| Legacy model | Current model | Why it changed |
|---|---|---|
Content | Work, Expression, Expression Unit, Manifestation | Separate stable legal identity, semantic versions, structure, and renditions |
| Category and SubCategory | Classification, Subject, Work Type, Work Source, Jurisdiction | Represent different legal classification dimensions explicitly |
| Favorite | Bookmark, Bookmark Item, Work Note | Preserve the context of legal research instead of a flat saved flag |
| Content views and counters | Monitoring consultations and HLL projections | Separate user history from aggregate read statistics that can grow with usage |
ContentManagement | Corpus and Monitoring | Separate editorial legal truth from user discovery and follow-up activity |
IdentityAndAccess | Identity and Access | Separate who the user is from what the user or organization may access |
SubscriptionBilling | Billing and Access | Separate money, orders, and provider events from entitlements and seats |
| One Symfony-rendered application | Symfony API plus main and admin TanStack Start applications | Let customer and operational workflows evolve independently |
| Browser/server session coupling | Same-origin BFF with server-side Symfony tokens | Keep access and refresh tokens out of browser storage |
| MariaDB | PostgreSQL | Support richer search, projections, indexing, JSONB, and HLL analytics |
| Manual cron entries | Symfony Scheduler and Messenger | Keep schedules in code and move slow work to observable queues |
The current platform passes identifiers, commands, read models, and domain events between contexts instead of sharing aggregates. Billing fulfills a paid order but does not mutate a subscription. Monitoring owns bookmarks and references Works by identity. Corpus does not own a user's favorites.
The frontend refactor followed the same principle. Instead of letting templates, controllers, session behavior, and product UI grow together, the platform now has a backend API and two dedicated React applications. The main application owns public discovery and self-service workflows. The admin application owns editorial and operational workflows. Shared packages provide API contracts, authentication workflows, search, the legal reader, editor behavior, UI primitives, analytics, and utilities without forcing both applications into the same screen architecture.
This refactor also gave us an opportunity to replace implicit behavior with explicit lifecycles:
The current model names the distinctions that the first platform exposed through production use.
System architecture
Leganews now uses seven business contexts:
- Corpus owns legal Works, Expressions, structured text, editorial review, attachments, and legal relations.
- Monitoring owns search, alerts, notifications, bookmarks, notes, consultations, and recent searches.
- Identity owns users, authentication, account lifecycle, federated identities, and security challenges.
- Access owns subscriptions, organizations, members, invitations, and direct Work access grants.
- Billing owns products, prices, orders, payment transactions, and provider events.
- Compare owns document and legal-version comparison workflows.
- Policy owns the platform's versioned terms and privacy documents.
The backend follows Domain-Driven Design and a layered architecture inside each bounded context:
Commands change state. Queries return purpose-built projections. Doctrine ORM persists aggregates, while Doctrine DBAL handles optimized searches, reports, tables, and cross-record read models.
Publication, search, subscriptions, and payments change for different reasons. Each context keeps its own terms and rules, and exchanges identifiers or events with the others.
Modeling a legal corpus
The hardest design problem was deciding what the platform actually stores.
A naive model could use a table named documents with a title, a PDF, and a publication date. That works until the same legal act receives a new version, an editor structures its content, a later text modifies it, or users need to compare two states of the law.
Leganews instead distinguishes four connected concepts: the stable legal Work, its semantic Expressions, their structural Expression Units, and the technical Manifestations used for reading or download.
This distinction is central to the platform.
A Work may be a normative act or a case-law decision. It owns stable metadata such as its title, type, source, classification, subject, publication date, legal reference, access policy, and current derived editorial state.
An Expression represents a version of that Work. Expressions have a kind, version number, expression date, validity period, structure state, editorial state, and summary. The current expression kinds are original and consolidated.
An Expression Unit represents the internal structure of the version: body, preamble, book, part, title, chapter, section, article, paragraph, alinea, point, or annex. Units form a hierarchy through parent identifiers and materialized paths.
A Manifestation is the technical rendition presented to a reader, such as structured HTML, Markdown, plain text, or a generated PDF.
One Work keeps a stable page while each Expression identifies the version being read. Validity periods, consolidation, comparisons, and citations attach to those separate records.
Editorial workflow
Legal information should not become public only because a record exists in the database.
Leganews uses an explicit editorial state machine for Expressions:
Editors can submit a structured Expression for review. Reviewers can approve it or request changes with a required comment. Approved content can then be published. Withdrawn or approved content can be reopened as a draft.
Publication is a business event, not a boolean update.
When an Expression becomes published, the platform can:
- derive the public editorial state of the Work;
- refresh the Work summary from the latest published Expression;
- build a readable Manifestation;
- project inline legal citations into Work relations;
- update the search document;
- generate comparisons with other published Expressions;
- schedule notifications for matching user alerts.
The Work's public state comes from its Expressions. An older published version remains visible when an editor creates a new draft.
The administration application supports this workflow with a legal-text editor and review controls. The editor stores an editing representation while the backend persists domain-level Expression Units separately. This keeps the user experience of editing distinct from the canonical structured model used for search, reading, export, and comparison.
Legal relations
Legal texts are not independent records.
One text may cite, amend, partially amend, repeal, partially repeal, implement, apply, ratify, interpret, consolidate, or replace another text. Treating these as generic hyperlinks would lose their legal meaning.
Leganews models them as Work Relations.
A confirmed relation records a source Work, a target Work, a relation type, and optional grounding in an Expression or Expression Unit. Some relations also change domain state. For example, applying a repeal relation to a normative act can update the target Work's status to repealed. A modifying relation can optionally initialize a consolidated Expression from the latest relevant content.
When an editor publishes an Expression, a handler projects its inline references into cites relations. Automated processes and import rules store proposed relations as Work Relation Suggestions with evidence and confidence, not as legal facts.
Suggestions and confirmed relations follow different paths:
Automation can help discover possible links. It should not be allowed to publish legal meaning without review.
Search and information retrieval
Once legal content is structured, the next challenge is retrieval.
Leganews does not search every normalized table for each request. A projection handler writes published Work data to search_documents, including the latest Expression, title, headings, body, legal references, and filterable metadata.
PostgreSQL provides the first search engine.
The search layer uses:
- French full-text configuration;
unaccentnormalization;- weighted
tsvectorvalues for titles, headings, and body text; - GIN indexes for full-text retrieval;
- trigram indexes for approximate fallback matching;
- ranked results with title and body highlights;
- filters for Work kind, type, source, jurisdiction, classification, subject, access, and date;
- explicit title-only, body-only, and full-text scopes.
The first attempt uses PostgreSQL full-text search. If it finds no results, the engine falls back to trigram similarity. This helps with spelling differences, incomplete references, and queries that do not tokenize cleanly.
Search is located in the Monitoring context rather than inside Corpus because search is part of the user's ongoing research activity. That same context owns recent searches, suggestions, saved Works, notes, consultations, and alerts.
The frontend shares one search module between the public and administration applications. Users can select a scope, apply legal filters, inspect highlighted fragments, and retain enough context to verify the result instead of treating a keyword match as a legal conclusion.
Monitoring, alerts, and personal research
Finding a text once is only one part of legal work. Professionals also need to return to a matter and follow it over time.
Leganews models monitoring through several focused capabilities:
Bookmarks are more useful than a flat favorites list because they preserve context. A user can organize Works by client, research question, litigation file, or monitoring subject. Personal notes stay attached to the Work so the reason for saving it is not lost.
Alerts can target a whole legal classification, one subject, or one specific Work. When an Expression is published, an asynchronous handler finds matching subscribed users in bounded batches. It deduplicates notifications, applies access policies, stores an unread notification, publishes a realtime Mercure event, and sends email when that channel is enabled.
The platform also supports a weekly monitoring digest for users who prefer a consolidated summary.
This turns publication into a product feedback loop. The editorial team adds or updates a legal source; the system makes it searchable; users following the relevant scope are informed without repeating the same search every day.
Document and version comparison
Legal work often involves comparing two versions of a document line by line.
Leganews supports two related comparison workflows:
- compare two published Expressions of the same Work;
- compare two private PDF or DOCX files uploaded by a subscribed user.
The uploaded-document workflow accepts two files of up to 10 MB each. Processing runs asynchronously because extraction and diff generation should not block the HTTP request.
For PDF files, the platform extracts UTF-8 text with pdftotext. For DOCX files, it reads the WordprocessingML document directly from the ZIP archive and preserves paragraph and table separation. The text is normalized before the operating system's unified diff algorithm produces additions, deletions, and contextual hunks.
The comparison aggregate owns a clear lifecycle:
The comparison worker removes temporary uploads after processing. A scheduled cleanup deletes results after seven days. Mercure and email report completion or failure to the user.
The implementation also handles operational edge cases: identical checksums, empty extracted text, tracked changes in DOCX files, oversized diffs, unsupported files, and safe failure reasons that do not leak internal details.
Access and billing
Legal content access is a separate domain from payment.
A payment records a commercial event. Access decides which content or feature the buyer may use.
Leganews supports two access paths:
When a user requests a protected Work, the access authorizer first checks for an active direct grant. If none exists, it evaluates subscription access, including organization coverage and privileged roles.
Billing owns products, prices, orders, order items, payment transactions, and provider events. Access owns subscriptions, organizations, memberships, invitations, seats, and Work grants. The two contexts collaborate through explicit fulfillment services and events.
The checkout flow supports local payment needs through FlexPay, including mobile money and card payments in CDF or USD.
Payment callbacks use idempotent processing. The handler computes a fingerprint for each provider event, ignores exact replays, locks the payment transaction, and prevents a second fulfillment. It validates every order item before creating entitlements and marks the order fulfilled only after all items succeed.
Billing records that the provider collected money. Access reads the fulfilled order and creates the corresponding entitlement.
API and web applications
Leganews has two React applications built with TanStack Start:
Both applications use React, TanStack Router, TanStack Query, Tailwind CSS, Zod, and shared workspace packages managed with Bun and Turborepo.
The browser does not store Symfony access or refresh tokens. Each TanStack Start application keeps them on the server and gives the browser an HTTP-only session cookie. Its /api route adds the access token, refreshes an expired session when possible, forwards the request to Symfony, and retries after a successful refresh.
The API also receives an X-Client-Platform header. Symfony uses controller metadata and a centralized guard to prevent the main application from calling administration routes or the administration application from using an unsupported route.
On the TypeScript side, @workspace/api exposes platform-specific clients:
Contracts are defined with Zod and TypeScript types are inferred from those schemas. The package hides raw URL construction, HTTP details, and query helpers behind small product-facing interfaces.
Requests cross this boundary:
The frontend can focus on user workflows. Symfony remains responsible for business decisions.
Migrating the legacy platform
Leganews is not a greenfield product. The initiative started years before the current architecture, and the new platform has to preserve useful data from the previous application.
The development environment runs both PostgreSQL and the legacy MariaDB database. A plan-based importer moves records in dependency order:
Each import plan maps one legacy concept to the new domain model, detects conflicts, supports dry runs and filters, and records whether each row was inserted, skipped, rejected, or failed.
Imports run in bounded batches so a large historical dataset does not have to fit in memory. The reset workflow can rebuild the PostgreSQL database, import legacy and seed data, rebuild search projections, reupload attachments to object storage, reconcile citation relations, and synchronize expired subscriptions.
The migration is not treated as a one-time SQL copy. The old schema and the new domain model do not express the same concepts. Mapping through explicit plans makes those differences visible and lets the new domain rules remain authoritative.
The import preserves historical data without carrying every old model into the new application.
Applied AI with editorial boundaries
Leganews uses AI in a deliberately narrow role.
When a published Expression has no editorial summary, the platform can generate a short French summary through a configured OpenAI or Gemini model. The prompt requires a factual summary without interpretation or information absent from the source. Input size is bounded, provider failures are logged, and the summary stores its provenance as manual, generated, or extracted.
If no provider is configured or generation fails, the publication workflow can fall back to a deterministic summary extracted from the Expression Units. Publication does not need to become unavailable because an external model is unavailable.
The same principle appears in legal relations. Automated systems may create relation suggestions with evidence and confidence, but a suggestion remains different from a confirmed legal relation.
The design rule is:
AI may accelerate editorial work, but it should not silently become the source of legal truth.
Reliability, observability, and testing
A platform that depends on background jobs, payments, external storage, realtime delivery, and editorial workflows needs more than successful HTTP responses.
Leganews uses Redis-backed Symfony Messenger for asynchronous work and failed-message storage. Scheduled jobs handle tasks such as expired subscriptions, comparison cleanup, monitoring digests, account cleanup, and monthly platform reports.
Operational signals are separated by purpose:
- structured rotating application logs;
- business audit logs;
- Sentry-compatible error capture;
- Telegram delivery for business-critical failures;
- correlation identifiers across buses and requests;
- health checks for operational dashboards;
- Google Analytics events behind privacy sanitization in the web applications.
Work consultation statistics use HyperLogLog for approximate unique-reader counts. This keeps aggregate read metrics useful without retaining a detailed anonymous history row for every visit.
Testing is also divided by responsibility:
Behat scenarios record product rules. Publishing an Expression makes a Work readable. The payment handler processes a duplicate webhook once. Monitoring stops alerts for expired subscribers. Access rejects archival of an organization with an active subscription.
The domain tests record rules such as publication transitions, entitlement creation, and idempotent payment fulfillment.
Key engineering decisions
Model legal meaning before files
The platform does not treat a PDF as the legal object. Work, Expression, Unit, and Manifestation separate stable identity, semantic version, structure, and technical rendition.
That model creates room for versioning, consolidation, search, citation, comparison, and future temporal queries.
Make publication an event
Publishing an Expression is the point where editorial work becomes product behavior. Search indexing, summaries, manifestations, citations, comparisons, and alerts all react to that transition.
This keeps the editor focused on editorial work while downstream capabilities stay decoupled.
Keep suggestions separate from legal facts
An AI-generated or imported relation suggestion is not a confirmed Work Relation. Evidence, confidence, and editorial validation provide an explicit boundary between assistance and authority.
Use PostgreSQL before adding a separate search cluster
PostgreSQL already provides French full-text search, ranking, highlights, trigrams, relational filters, and indexes. A separate search service would add deployment and synchronization cost before the product has proved that PostgreSQL is insufficient.
Separate billing from access
Payment success does not directly mean "show the content." Billing records commercial truth; Access grants product entitlements. Idempotent fulfillment connects them.
Keep tokens out of browser storage
The TanStack Start BFF owns each application session, forwards same-origin requests, and refreshes Symfony tokens server-side. The main and admin sessions remain separate.
Treat legacy migration as domain translation
The previous MariaDB schema is a source, not the target model. Ordered import plans translate old records into the current domain and report conflicts explicitly.
Use asynchronous processing where the user should not wait
Document extraction, comparisons, alerts, email, and other background workflows run through Messenger. Mercure closes the feedback loop by updating the interface when processing changes state.
Use AI as a replaceable adapter
Summary generation sits behind an application interface, supports more than one provider, stores provenance, and has a deterministic fallback. The core corpus does not depend on one AI vendor.
What I learned
The biggest lesson from Leganews is that uploading files does not solve legal access. The system has to preserve where a text came from, which version a person is reading, how it is structured, what it changes, and whether the source supports the decision at hand.
Being a co-founder changed how I approached the refactor. I could not evaluate the system only as an engineer. Every architectural change had to protect years of editorial work, existing users, commercial rules, and the trust attached to the Leganews name. The best design was not the cleanest model in isolation; it was the model that could move the product forward without pretending its history did not exist.
I also learned that editorial workflow is part of the architecture. Public search can only be as good as the private process for creating, reviewing, correcting, approving, publishing, and withdrawing content.
Search became more important than expected. Once the corpus contained structured content, the next challenge was not storage but retrieval: French language normalization, ranking, fuzzy fallback, filters, highlights, and enough metadata for users to judge a result.
Monitoring changed the product direction as well. Legal research is rarely a single query. It is an ongoing activity. Bookmarks, notes, recent searches, alerts, realtime notifications, and weekly digests turn the corpus into a workspace rather than a static library.
The migration preserved business history without carrying every old abstraction forward. Import plans translated each legacy record and logged rows that could not be mapped safely.
Payments taught the importance of idempotency. Providers retry callbacks, users repeat actions, and distributed workflows can fail between steps. Stable event fingerprints, record locks, preflight validation, and idempotent fulfillment are what make the commercial flow trustworthy.
AI is useful here only when its authority is limited. Generated summaries and relation suggestions can reduce editorial work, but the platform records their source and keeps a human approval step.
Current work
Leganews Pro now connects the legal corpus, editorial review, indexed search, monitoring, document comparison, billing, and organization access. The migration also has to protect years of content and commercial history from the first application.
My current work is less about adding isolated features and more about keeping those boundaries sound. A payment must grant the right access. A published version must update search and alerts. An imported relation must remain a suggestion until an editor confirms it.
The purpose has stayed the same since 2019: shorten the path between a legal question and the sources needed to answer it.