SaaS is dead. You've heard all the AI hype crew say this. I think it's a bit more nuanced -- and you'll see that for yourself as you read this post -- but what is true is that the way software is built, bundled, and sold is significantly changing.
Two truths for me:
- Product features are easily replicable with a few prompts in agentic coding tools like Claude Code or Codex. The moat, and high margin / lock in of SaaS, is slowly eroding. I like to pick on Salesforce as an example. $125/user/month (starting!) is absolutely ridiculous for a basic CRUD, kinda clunky, CRM. But they have a huge ecosystem and big vendor lock in, so they can continue paying the debt on their tower in SF, for now.
- Sales Engineers are uniquely positioned in this new world to create their own homegrown sales automation tools. They have the technical chops, huge customer/user empathy, and a deep understanding of go-to-market systems.
Here are the blueprints of OurFX if you're curious about the system design (as a customer) or want to build it yourself (DIY) as a homegrown system. And if you're a competitor, you can rip it off, speed (ship early & often) and closeness to customers is the only product moat anyways. And if you're interested in collaborating, let me know, my email is john@aero.ws! We're designing an open-core community of builders.
What's in here:
- Data model
- Language modeling
- Workflow design
- Tech stack
Data model
- Companies: A prospect or client is the center of the hub, always has been, for any sales tool. We technically don't need it to process RFPs but salesOps is going to ask for it anyways, so here you go.
- RFX: The RFX, the container for RFPs, RFQs, Security Questionnaires, DDQs -- requests for anything. (Many RFX to one Company).
- Requirements: A child of RFX (many:one) This is the most atomic unit, singular thing, that a prospect needs. An answer to a question, a specific integration. This is what you vectorize and compare to historical requirements.
- Product: The product(s) that the company creates and sells.
- Feature: A child of product (many:one)
Here's the SQL
CREATE TABLE "companies" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar(255) NOT NULL,
"description" text,
"contact_name" varchar(255),
"contact_email" varchar(255),
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
CREATE TABLE "rfps" (
"id" serial PRIMARY KEY NOT NULL,
"title" varchar(255) NOT NULL,
"description" text,
"owner_id" integer NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
"company_id" integer,
"product_id" integer
);
CREATE TABLE "requirements" (
"id" serial PRIMARY KEY NOT NULL,
"requirement_number" varchar(50),
"category" varchar(255),
"requirement_title" varchar(500),
"requirement_description" text,
"updated_by" integer NOT NULL,
"vector" vector(1536),
"rfp_id" integer NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
CREATE TABLE "products" (
"id" serial PRIMARY KEY NOT NULL,
"product_name" varchar(255) NOT NULL,
"product_owner" varchar(255) NOT NULL,
"product_features" jsonb DEFAULT '[]'::jsonb,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
CREATE TABLE "product_features" (
"id" serial PRIMARY KEY NOT NULL,
"feature_number" varchar(50),
"category" varchar(255),
"feature" text,
"description" text,
"last_updated" timestamp DEFAULT now() NOT NULL,
"updated_by" integer NOT NULL,
"vector" vector(1536),
"product_id" integer NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
Language modeling
- Model selection: Holds what model provider (Claude, ChatGPT), and what model (like Sonnet 4.5 or GPT-5.2)
- Prompt: This is the
system promptthat the tool uses as the "SE Persona" - it has your role, approach, any language rules for voice and tone (though the LLM will pick up on well-rated historical responses as well), brand guidelines. This is how we shape it to sound like your best SEs and from your company, so that it's unique, high-quality, and differentiated (kill the "it's not this, it's that") - Match, feature & historical RFPs: Each requirement, when processed, will use vector similarity search (cosine inner similarity) to find the top matching product features and historical RFX responses.
- Company and RFX details: pass into the prompt.
OurFX sends this info as a bundle to the LLM. It's grounded, it's in context, it's guided by rules. And what comes back is a really high-quality response.
Here's the typescript template:
const prompt = {
model: "anthropic-sonnet-4.6",
message: {
systemPrompt: "You're a B2B SaaS sales engineer....",
userPrompt: {
requirement: requirement.text,
company: company.details,
productMatches: product.matches(3),
historicalMatches: historical.matches(3),
}
}
}
Workflow design
You'll need to give your users a way to process the RFX end-to-end. There needs to be a UX to support the following:
- Create a company (manually or integrated with your CRM).
- Create/upload an RFX. Add the RFX, upload a doc/csv, it processes it and turns it into discrete requirements.
- Run a go/no go on Product and historical RFX matches.
- If go, run the responses, one at a time, or as a whole.
Tech stack
- Database: Postgres, w/ pgvector
- Front end: React w/ Shadcn/ui components (leveraging Tailwind CSS)
- Models: Anthropic or OpenAI (LLM and embeddings)
- SDKs: AI SDK, OpenAI Agent SDK