Skip to content
vp.Ven Petrov
WorkWritingAboutAt a glance
← Projects & notes
Sociedades Inactivas enquiriesTechnical note · 3 min read

Saving an enquiry before trying to send its email

On the Sociedades Inactivas catalogue, a visitor can ask about a company or send a general enquiry. The form separates saving that request from delivering its notification, including when the visitor retries.

Ven PetrovImplemented; live activation pending

Sociedades Inactivas is a Next.js website with a catalogue of already incorporated Spanish companies. A visitor can select a company and ask about it, or send a general enquiry.

The form has two jobs: save the request and notify the people who need it. Email can fail after the database has already saved the message. If that failure makes the visitor start again, the application can receive the same enquiry twice.

The implemented response contract is simpler for the visitor: success means the request has a stored reference. Email delivery has its own state.

One message through a failed notification

This is the failure case handled by the code, rather than a claim that a particular customer lost an enquiry:

Event What the application keeps
Valid form arrives Enquiry and pending-delivery record are saved together
First email attempt fails The same enquiry remains saved; delivery stays pending
Visitor repeats the same request The original reference is returned
Same request key arrives with changed content A conflict is returned instead of silently using the old message

The pending-delivery record is an outbox: work saved in the database so a notification can be retried later. It is created in the same transaction as the enquiry. A transaction makes those writes succeed or roll back together.

The tests force the outbox insert to fail. The enquiry and its rate-limit increments must roll back too, so a saved request cannot be left without its delivery work.

The database commits before the email attempt

  1. 01Submission keyCheck the key and payload fingerprint
  2. 02TransactionSave enquiry and delivery record together
  3. 03SMTP attemptClaim the record with a lease token
  4. 04Saved referenceReturn it even if email fails
A failed SMTP attempt leaves the saved enquiry and its pending delivery record available for retry.

A retry must mean the same request

The browser sends a submission key. The server stores a hash of that key and a fingerprint, a hash representing the validated message content.

Matching key and content return the existing reference before charging the submission quota again. Matching key with different content returns HTTP 409, a conflict. The same key cannot quietly identify two different messages.

A selected company is also checked against the current catalogue inside the transaction. The stored company name comes from that lookup, not arbitrary browser text. If it has been removed or sold, the visitor can remove the selection and send a general enquiry.

What happens after the commit

The API waits for its first email attempt after saving. A mail error is caught without discarding the saved reference.

For retries, a worker first claims the delivery record with a lease token. The lease is a temporary ownership claim, here lasting 60 seconds. Another worker should leave the record alone while that claim is current. A failed attempt is rescheduled with increasing delays, capped at six hours, and the final update checks that the token still matches.

This does not guarantee exactly one email: a mail server may accept a message just before its response is lost, or a lease may expire during a slow send. The enquiry's storage and the notification's final state remain different facts.

Eleven focused tests cover storage, duplicate requests, rollback and delivery behavior using disposable databases and mocked mail. Production activation is a separate unfinished step: the deployment record has the form returning 503 before saving while security and mail configuration were deferred. This note describes the implemented flow and its tests, not successful live enquiries.

Project sources & checks
Local repository and code review

sociedades_inactivas_new_web: app/api/contact/route.ts, lib/db/contacts.ts, lib/contact/delivery.ts and tests/contact.test.mjs.

Focused verification

All eleven existing contact tests passed locally, using test databases and mocked delivery.

Dated deployment record

docs/PROJECT-STATE.md records the production contact route returning 503 before storage, with security configuration and mail setup deferred.

On this page
One message through a failed notificationA retry must mean the same requestWhat happens after the commitSources & checks
Next.jsTypeScriptTursoReliability

Connected to this

All writing
3D Spaces website migration · Project story

Moving 3D Spaces to Next.js while its email stayed on the old server

Capture CRM · Project story

Connecting classified listings to contacts in a small CRM

Ven PetrovSoftware engineer · Barcelona
GitHub ↗LinkedIn ↗Email ↗

Find a project or a note

17 results

Transfutur ERP · ExperienceERP for transport and logisticsAs the sole engineer, I built an ERP from zero for transport and logistics. It supported roughly €5–6 million in annual invoicing across two offices, and I kept it running for about a year.Financial review lab · Project storyExploring a financial report with Python and FastAPII started with one income change, then opened up the same 54-page report: products, regions, cash flow, core adjustments and a dataset whose figures lead back to the source.Crude-oil narrative tracker · Project storyCrude-oil research from global news and market dataI wanted help following global events that could matter for crude oil. The prototype combines news research with calculated market and position values, while keeping recent assessments separate from the longer numeric history.MNQ time-series research · ExperimentTime-series prediction experimentsAn exploration of high-resolution market data: choosing what to acquire, checking its limits, comparing time intervals and testing whether a prediction survived later data and the cost of acting on it.NQ/MNQ backtest engine · Technical noteA missing closing bar left the simulator’s short position openThe strategy reset for a new day while a shared counter still counted yesterday’s short. A small replay shows why closing the position has to happen before clearing its state.Small-cap edge research · Project storyTurning small-cap trading ideas into repeatable researchI asked Claude Code for a fresh research system that could test named price patterns, compare later periods and carry its methodology into new sessions. One candidate shows how features, entry timing and costs fit together.Gadgeon Europe applications · ExperienceConnected-device dashboards and mobile apps at Gadgeon EuropeI worked on Ionic and Angular applications that brought several APIs into dashboards for energy monitoring and connected-device control, with releases and maintenance across multiple client versions.ctxforge · Project storyctxforge: discovering what to build before codingA context discovery framework that asks an LLM to act like a senior engineering interviewer: understand the idea, uncover requirements and agree the scope before an agent starts building.Sociedades Inactivas enquiries · Technical noteSaving an enquiry before trying to send its emailOn the Sociedades Inactivas catalogue, a visitor can ask about a company or send a general enquiry. The form separates saving that request from delivering its notification, including when the visitor retries.Astro local-services website · Project storyA local-services site assembled from city, service and FAQ recordsThe Astro site describes flooring and other local services across different cities. Shared templates check availability, read the matching content and select one FAQ set for both visitors and structured data.3D Spaces website migration · Project storyMoving 3D Spaces to Next.js while its email stayed on the old serverI rebuilt the virtual-tour website with AI coding collaborators, preserving its content and URLs. The cutover also had to protect the mailbox, enquiry flow and existing analytics history.Capture CRM · Project storyConnecting classified listings to contacts in a small CRMA Chrome extension and Next.js dashboard bring classified listings, platform profiles and contact history together. Saving a listing again can add the person who was missing from an earlier import.Product-description comparison bench · ExperimentComparing one model call with separate vision and writing stepsA small product-description tool lets me inspect the visual analysis before the copy is written, change the model used for each step and compare request measurements.Between Us · Project storyBetween Us: why I kept the first take and widened the shotI compared dialogue takes, asked for wider shots and more physical acting, then changed the workflow when its studio limited the scenes I wanted to make.Restaurant-service voice agent · ExperimentDesigning a restaurant-service call around the real offerI worked through a Spanish outbound agent prompt, corrected a free visit to the actual paid service and compared how two drafts handled transfers and bookings.Vapi and Telnyx voice integration · Technical noteThe Vapi agent could speak, but it could not hear me clearlyMy Telnyx number connected to the assistant, but the caller audio was distorted. Three codec tests recovered the connection without fixing that original problem.Warloop: Echoes of Solmara · Project storyWarloop: turning generated character art into a playable prototypeGood-looking character images became tiny figures, a backwards walk and misplaced bosses in the game. I worked through the gap between generated art and playable behavior.
esc to close · Tab to move through results