jp ferreira
← All writing
JP Ferreira8 min read

The model was the easy part: shipping an AI assistant into Invitify

What it took to put a plain-language event-setup assistant in front of real users - the guardrails, the gates, the one rule it could never break, and why almost none of the work was about the model.

  • AI
  • Trust & safety
  • Product engineering

// 01

The form was the friction

Invitify's create-event wizard opens with a basics step: event title, date, time, venue, host name, a template, a dress code. None of it is hard. All of it is tedious. It's the kind of form people start, half-fill, and abandon - not because they don't know what their event is, but because turning something they already hold in their head into eight discrete fields is friction they didn't ask for.

The thing is, they can already describe the event in a single sentence. "I'm having a birthday dinner at Chin Chin on Saturday the 12th for 20 people" contains almost everything the form wants. So the assistant lets them type that sentence. It appears as a floating button on the first step, reads the plain-language description, extracts the structured fields, and silently fills the form in behind the chat. When it has enough to work with, it signals that it's ready.

That's the UX win, and it's a real one: a form becomes a conversation. But the moment you let users talk to a language model inside your product, you've opened a door. Almost all of the engineering that followed was about what comes through it.

// 02

It fills the form. It never presses the button.

There was one rule the feature was never allowed to break: the assistant drafts, the person decides.

In practice that means the assistant extracts fields and populates them, and then stops. It never submits the form. It never advances the wizard. When it's done, it doesn't quietly move the user forward - it tells them that's everything it needs, to close the chat, take a look at the form, and click Continue when they're happy. The user reviews every prefilled field before anything happens. Continue is theirs to press.

This matters most with the fields a model is happy to be confidently wrong about. Ask it for a venue and it will produce an address that looks exactly right and isn't. So the assistant is framed, in the product and in the Terms, as a drafting aid: AI-suggested content - addresses especially - is the user's to verify before they publish. The model proposes. The human owns what goes out under their name.

It sounds like a small distinction - autofill versus submit. It's the whole thing. The assistant is a faster way to fill in a form, not a system that makes decisions on a user's behalf. Every other safeguard I built sits on top of that one.

// 03

Defense in depth for a feature most people use once

A plain-language chat box that calls a model is, the moment it's public, a free proxy for anyone who finds it. A cover-image upload is an open pipe to your storage bucket. Neither of those is hypothetical once the feature ships. So the assistant is wrapped in more machinery than its size suggests.

Authentication comes first. Each session is issued a signed HMAC token with a 24-hour lifetime, and every chat request has to carry it in a header. Requests without a valid token are rejected before a single OpenAI call is made - the cheapest possible place to say no.

Then there's a daily message cap, tracked in Redis, scoped to the user's auth ID, resetting at midnight UTC. It exists so that a valid session can't be turned into an unbounded bill.

Then moderation, in two places. Gate A runs on chat input: before the language model ever sees a message, that message goes to OpenAI's moderation endpoint - a separate, free API - and if it's flagged, the assistant returns a refusal and the chat model is never called. Gate B runs at the publish boundary: when a host hits Publish, the event's title, description, host name, dress code, and venue name are batch-checked, and if any field is flagged the publish is rejected with a 422 and the event stays a draft. Nothing reaches the public page on the strength of having slipped past the chat.

Images get their own treatment. Cover images no longer go straight from the browser to S3. They route through the Django server, which reads the bytes, sends them to the same moderation endpoint as a base64 data URL, and applies per-category confidence thresholds - stricter for the categories that matter most, looser for the ones that don't. Only images that pass are forwarded to storage; flagged ones are refused with a clear error before they're ever written.

None of this is exotic. That's rather the point. The model is the least of the system.

// 04

Moderate before you count

The detail I'm quietest-proud of isn't a component. It's an ordering.

The daily message counter only increments after moderation passes. If a user's message is flagged at Gate A, the request is rejected before the counter ticks and before the chat model is called. Three things fall out of that single sequencing decision.

First, abusive content doesn't burn the user's own quota. It would be a strange kind of punishment to count a message against someone that you refused to even process. Reject it, and leave their allowance untouched.

Second, you never pay for a chat completion on content you were always going to throw away. The flagged message stops at a free endpoint and never reaches the metered one.

Third - and this one matters more than it might look - it protects the API key. The key is mine. The user typed the message, but if that message causes OpenAI to flag the account, the consequence lands on me, not them. OpenAI's policies are between me and OpenAI; the user isn't party to that relationship. Screening before the key is ever used is the only way to make sure I'm not held responsible for content I had no hand in producing.

It's a few lines of ordering and it doesn't show up in a demo. But the difference between moderate-then-count and count-then-moderate is the difference between a feature that behaves sensibly under abuse and one that quietly leaks money and goodwill the first time someone pokes it. Production-readiness lives in details like this far more than in the headline.

// 05

Fail open, and be honest about it

Here's the decision I went back and forth on. What should happen when the moderation API itself is down?

There are two honest answers. Fail closed: if you can't check content, you block it. Fail open: if you can't check content, you let it through. I chose fail open. If the moderation endpoint errors or is unreachable, all three gates proceed as normal.

The reasoning is about proportionality. Invitify is an event-invitation app, not a public platform with a billion-item firehose. A moderation provider having a bad afternoon shouldn't mean no one in the world can create a birthday dinner. For this product, at this scale, taking the whole feature offline to guarantee screening is a worse failure than letting it run unscreened for the length of an outage.

But fail-open is a risk acceptance, not a free lunch, and I'd rather name it than dress it up. During an outage, content that would normally be caught could get through. So the gates aren't the only thing standing there: the daily cap and a per-IP upload throttle stay up as backstops regardless of moderation's health, and the human-review checkpoint at publish never goes anywhere. The thing I most rely on during a moderation outage isn't another automated gate - it's that a person still has to look at the form and press the button.

Two more layers sit underneath that. The system prompt sent with every chat request includes explicit rules that keep the assistant scoped to event setup - it won't discuss unrelated topics, won't follow instructions to behave differently, and won't produce content outside that narrow task. And beyond anything I've built, OpenAI's own usage policies apply to every request regardless of whether my moderation gate is running. Fail-open doesn't mean unprotected. It means I've thought about what's still standing when one layer goes down.

I'd make the same call again at this scale. I'd revisit it at a larger one. The useful part of writing this down is the tradeoff, not the conclusion.

// 06

Say what it does

The least technical part of the build is the one I'd defend hardest.

Two things leave the app and go to a third party: the chat conversation and the moderation requests. Users are owed that information in language they can actually read. So the Privacy Policy says it plainly - what gets sent to OpenAI, that OpenAI doesn't train on API data, that it's retained for around thirty days for abuse monitoring. The Terms spell out what's prohibited and state that automated screening runs at both upload and publish.

None of this is legally clever, and that's deliberate. The failure mode for AI disclosure usually isn't lying - it's burying. You can be technically transparent and functionally opaque by writing it the way a contract is written. Putting a model in front of users and then telling them, in plain terms, what it does with their words is the floor, not a favour.

// 07

What I took away

Three things, beyond this one feature.

One: the model was the easy part. The call to OpenAI is a few lines. Everything that made the feature safe to ship in front of strangers - the token, the cap, the two gates, the image pipeline, the ordering, the fail-open reasoning, the disclosure - is the actual work. The value of an AI feature is almost never in the model. It's in the system you build around it.

Two: the human checkpoint isn't a UX nicety, it's the safety architecture. The assistant fills the form; the person presses the button. Take that away and every other guardrail is suddenly doing far more work than it was designed for, because there's no longer a human standing between the model's output and the world.

Three: build the guardrails as if the feature will be abused, and write the disclosure as if your most skeptical user will read every word. Do both and you end up with something you can leave running without watching it - which, for a solo-built product with real users, is the only kind of feature worth shipping.