Skip to content
Aiden Azraya, home

AI · SaaS · Full-stack

ReplyCurrent

ReplyCurrent indexes a company's website, help centre and files, then answers visitor questions from that material — with the source it used attached to the reply.

Role
Sole developer — product design, frontend, database schema, Edge Functions, billing
Year
2026
  • Next.js 16
  • React 19
  • TypeScript
  • Supabase Edge Functions (Deno)
  • PostgreSQL 17
  • pgvector
  • CSS Modules
  • Stripe
replycurrent.com
The ReplyCurrent home page, showing the headline and an embedded assistant answering a pricing question.

What it does

Most of the answers a visitor wants are already published. The price is on the pricing page, the setup steps are in the help centre, the refund rule is three paragraphs into a PDF. The visitor is the one expected to assemble them.

ReplyCurrent takes those sources, indexes them, and answers in one reply. The answer carries the page it was drawn from, so the person reading it can check the claim rather than take it on trust.

Built for small SaaS teams, online shops and agencies that have written the documentation already and do not have someone sitting on a support inbox all day.

What I did

I am the sole author of every commit in the repository. There is no team, no agency and no template behind it.

I built the marketing site and the dashboard as a static Next.js export, styled with hand-written CSS Modules rather than a component library, and wrote the typed data layer that is the only code allowed to talk to Supabase.

On the backend I wrote the ten Deno Edge Functions, the crawler, the retrieval pipeline, the Postgres schema with row-level security on every table, and the Stripe subscription gate.

Parts of the work were written with AI assistance, as most of my work now is. The architecture, the data model and the decisions described on this page are mine.

Commits, all mine
37
Edge Functions
10
Hand-written CSS Modules
109
Unit test files
59

How it is put together

There is no application server. The Next.js deployment is static files; everything that runs lives in Postgres and in Deno Edge Functions next to it.

Getting the knowledge in

  1. Website, help centre or uploaded file

    The customer names a source in the dashboard.

  2. Crawl

    Own crawler in Deno: robots.txt under its own product token, sitemap discovery, breadth-first walk, and a guard that refuses any address resolving to a private network.

  3. Chunk

    Section-aware. Each heading opens a section, chunks run to about 1,100 characters with a 120-character overlap, and every chunk is prefixed with its page title and heading.

  4. Embed

    gte-small, 384 dimensions, computed inside the Edge Function itself — no external embedding key is needed.

  5. PostgreSQL 17 + pgvector

    Chunks are stored against the workspace that owns them.

Getting an answer out

  1. Visitor's question

    Answered by a public function gated on publish state, a domain allowlist and a per-minute rate limit — never on who is asking.

  2. Retrieve

    Cosine match over pgvector, minimum similarity 0.35, top eight. If that returns fewer than three usable chunks, a lexical search adds to them.

  3. Ground

    Context is capped at 6,000 characters. The system prompt marks the retrieved material as untrusted content rather than instructions, and gives the model an explicit way to say it does not know.

  4. Complete

    OpenAI, Anthropic or Google, chosen per workspace from a server-side allowlist, using either the platform key or the customer's own encrypted key.

  5. Reply, with sources

    The conversation, the messages and the chunks that were cited are written back.

Drawn from supabase/functions/_shared/rag.ts, crawler.ts and the project's own architecture notes.

What is in it

Chunks that still know what they are about

A price split out of its page is useless: "$99 a month" does not say which plan. Every chunk is written with its page title and section heading at the top, so it survives being retrieved on its own.

chunkText() — supabase/functions/_shared/rag.ts

Hybrid retrieval

Vector search handles paraphrase well and exact strings badly. When the cosine match returns fewer than three usable chunks, a keyword search over the same table fills the gap before anything reaches the model.

match_knowledge_chunks, then search_knowledge_chunks

An answer it is allowed to refuse

The prompt gives the model a token for "this is not in the material". A support assistant that invents a refund policy is worse than one that says it does not know.

Customer keys, encrypted

A workspace can bring its own provider key. Keys are encrypted with AES-GCM and excluded from the column grants, so the browser can never read one back out.

A crawler that works in slices

An Edge Function has a short life. The worker takes a budget of work units per invocation, checkpoints its cursor into the job row, and starts a fresh copy of itself while there is more to do. A scheduled job inside Postgres picks up anything that stalls.

discover → fetch → embed → finalize

Isolation in the database, not the browser

Row-level security is on every table, and a trigger in Postgres caps what a workspace without a subscription can do. The client-side gate is a convenience; the database is the rule.

replycurrent.com
The interactive demo: a question about annual plans answered with a citation back to the pricing page.
The demo on the home page. The answer carries the page it came from, so a visitor can check the claim.

What was hard

Retrieved text is somebody else's text

A crawler pulls in whatever is on the page, and some of that will be written to be read by a model. The prompt states plainly that the reference material is content to be used, not instructions to be followed, and the crawler refuses addresses that resolve to private networks so a customer cannot point it at an internal host.

Not deployed is different from failed

The dashboard calls Edge Functions, but a function can be missing. The invoker tells the two apart by re-asking with an invalid action: a function that answers an error is believed, and only genuine absence is routed around to the database path. That distinction matters because the alternative — retrying blind — repeats writes that already landed.

Fitting ingestion into a function's budget

Crawling a site does not fit in one invocation. Splitting it into budgeted slices with a checkpointed cursor made the work resumable, and made a stalled job something a scheduled sweep can recover rather than something a person has to notice.

Where it stands

  • The product is live at replycurrent.com, including the interactive demo on the home page.
  • Ten Edge Functions, a Postgres schema with row-level security on every table, and 59 unit test files run under node --test.
  • The front end is 109 hand-written CSS Modules — no Tailwind, no component library.

Next case study

RetainCue

A hosted cancellation flow that makes an offer and then proves Stripe applied it.