Most firms searching the phrase "custom M&A advisory platform" land on two kinds of pages. One is a vendor list (DealCloud, Affinity, Intapp, Datasite, Ansarada), each priced into the high five or low six figures annually. The other is a generic "CRM for advisors" explainer that never shows what the build contains.
This post is the other thing. A middle-market M&A advisory firm hired us last year to replace an outdated marketing site, a manual prospecting motion, and a CRM-by-spreadsheet workflow with one platform they would own outright. Below is what we shipped, on what stack, for what monthly bill, and the parts we'd redo if we started again.
What "custom" actually meant in this engagement
Three asks from the firm. A public site that ranked, an admin dashboard non-technical staff could run without calling us back, and a sourcing pipeline that found qualified targets automatically. Non-negotiable: every byte ran in the firm's own AWS account, infra under $100 a month, no five-hundred-dollar SaaS seats and no vendor handing them a quote at renewal.
Custom did not mean "we built a CRM from scratch." It meant the systems the firm actually needed were assembled into one codebase, deployed from one pipeline, owned end-to-end. The closest off-the-shelf equivalents (Axial for sourcing, an Intapp-style relationship CRM, a separate site builder, a separate analytics tool) would have summed to more per month than the entire AWS bill.
The architecture, in one paragraph
Seven AWS CDK stacks, one TypeScript monorepo, deployed through CodePipeline: Network, Cognito, Stateful, LeadGenPipeline, Api, LeadGenWeb, FlagshipWeb. Cross-stack values flow through props, not SSM parameter hacks. Compute is Fargate ARM64 (Graviton) everywhere it can be. The database is a single db.t4g.small running RDS PostgreSQL 16 with Prisma on top. Auth is Cognito with three roles and JWT validation in the API. The NAT gateway, which on AWS default pricing would have cost more than the rest of the infra combined, got replaced with fck-nat instances and dropped from around $30 a month to about $4.
That paragraph is the whole stack. Everything else is what each piece contains.
The public site and why it's server-rendered
The marketing site is Next.js 14 behind CloudFront. Every page pulls content from the database at request time. Titles, hero copy, CTAs, the team roster, the transaction history, even the firm's name. The admin can change any of it without a redeploy.
Dynamic rendering on every public route, on purpose. The site updates often (a closed deal, a press mention, a new team member) and the client expects changes to appear immediately. Incremental Static Regeneration would have been the textbook Next.js answer, but the cache-invalidation surface area wasn't worth it for a site measured in hundreds of visits a day, not millions.
SEO got more attention than most developers give it. Every page generates its own metadata with proper canonicals, OG tags, and Twitter cards. The sitemap enumerates every dynamic route, including the industry, state, city, and year grouping pages. JSON-LD structured data is on every page that warrants it: FAQPage, NewsArticle, Person for the team, Service and HowTo on the advisory pages, BreadcrumbList on detail routes. Google can't show your FAQ in search results if it doesn't know they're FAQ answers.
The deal-detail pages are the part we are most pleased with. Each closed transaction gets its own indexed page with structured metadata for the seller, buyer, sponsor, industry, and year. Those pages cross-link to industry, state, and city collection pages. Search a long-tail query like "manufacturing acquisitions in [state] 2024" and the firm's tombstone has a real shot at ranking.
The admin dashboard, in the same Next.js app
The admin lives at /admin as a route group inside the same Next.js app, gated by Cognito and validated server-side in middleware. Non-technical staff publish deal tombstones, write news and resources with inline editing, and manage team members, FAQs, sectors, and services without touching a CMS.
Tables use server-side pagination with debounced search, so they don't choke at hundreds of records. Deletes are type-to-confirm in the AWS console style: the user types the name of the thing they're removing before the button enables. Overkill for a firm this size, arguably. It's already prevented one accidental deletion.
Role-based access is enforced in both the UI and the API. Readonly users see a view icon where the edit pencil would be. Three roles, one source of truth in Cognito.
The lead-generation pipeline
This is the part that justifies calling the thing a platform rather than a website.
The pipeline is a Step Functions state machine fanning out to three Fargate tasks. The first queries the Google Places API for businesses matching the firm's configurable thesis, handles pagination and rate limits, and dedupes against what the database already holds. The second visits each website with a headless browser, extracts revenue signals, employee count, ownership indicators, and contact info, then writes the enriched record back. The third sends the enriched lead to Amazon Bedrock for qualification scoring against the firm's investment criteria. SQS queues sit between stages for backpressure.
Two Bedrock models, not one, and the split matters. Amazon Nova does the extraction and normalization pass: fast, cheap, fine for "turn this messy scrape into structured fields." Claude Haiku 3.5 does the qualification scoring, because judgment is where the cheaper model breaks down and Haiku was the cost-quality sweet spot for short-form classification at the time we built this. The scoring prompt took several iterations before the firm's analysts trusted the output.
The lead-gen frontend is a Vite + React SPA behind its own CloudFront distribution. Analysts search, filter, batch-tag, and export to CSV. Infra for the pipeline runs roughly $10 a month at idle and scales with usage. The real cost driver is Google Places API fees, which dominate the per-lead cost at scale.
Infrastructure as code, every line of it
Every resource in this account is defined in CDK. Not "we used CDK for the VPC and then clicked around for the rest." Every bucket, every role, every queue.
One construct in the codebase started as a workaround and is reusable on any CDK build with the same shape: TokenInjectableDockerBuilder, an open-source CDK construct published to npm and PyPI. CDK's native DockerImageAsset can't accept CDK tokens as build args, because Docker builds happen at synth time, before token values resolve. Ours defers the Docker build to deployment using CodeBuild, so cross-stack references and SSM parameters pass straight into the image.
The pipeline is self-mutating. A push to main triggers CodePipeline, which updates itself if the pipeline definition changed and then deploys the seven stacks in dependency order, no manual cdk deploy and no console clicks. The infra is a pull request away from changing, same as the application code.
What we'd do differently
The API layer. The Express service runs on Fargate twenty-four seven, best-in-class on availability and more compute than the actual traffic justifies. Lambda with Web Adapter would scale to zero at this volume. We've already moved the ingress from ALB to API Gateway v2 with VPC Link, so the path is clear. For read-heavy public pages, Next.js could talk to the database directly and skip the API hop.
Tests are the other one. The CDK stacks have synthesis tests and the critical Lambda paths are covered. The Express handlers could use more integration coverage. That was a conscious trade for shipping speed, not an oversight.
Build, buy, or rent
The question worth asking before a platform build is which layer of the stack is actually load-bearing for your firm.
Buy makes sense where the vendor has reached scale you cannot replicate. A CRM with bidirectional Outlook sync, mobile clients, SOC2, and an integrations team is a buy. So is anything where the data the vendor accumulates across all its customers is the product (a comp database, a fund-of-funds tracker). You are paying for their customer base as much as their software.
Rent makes sense for capabilities you need for one deal cycle and not the next. Virtual data rooms are the cleanest example. The contract starts at LOI and ends at close. Nobody should be building one.
Build is correct in exactly one place: the layer where your firm's workflow lives. For the firm in this post, that meant the sourcing pipeline (thesis lives in the prompts and the scoring weights), the deal tombstones (SEO and brand live in the structured data on those pages), and the admin (the team's daily motion lives in those forms). Renting any of those three would have meant pretending the firm's thesis was generic and letting a vendor own the URLs that ranked, which is the position they had been in.
A firm that rents its workflow layer never gets to compound on it. Every reseat, every new analyst, every prompt iteration improves the vendor's product, not the firm's. Five years in, the firm has a renewal quote and the vendor has a moat. This platform took a few months to build, runs for less than one seat of a competing tool, and every line of it belongs to the firm that paid for it.
