Invitations need notifications
An owner invites a teammate to their organization. The teammate has no account yet, so there is no way to reach them inside the app, and reaching them outside it means sending email.
Sending email is a system, not a function call.
Why invitations exist at all
Giant Context is multitenant. A customer is an organization, and organizations are sealed off from each other completely. People are not, because real work crosses those boundaries. A freelance designer builds sites for six different companies. An agency runs twenty client accounts. A consultant drops into a project for a month and leaves. Requiring a new account for every organization a person touches would describe a way of working that nobody has.
So the model is one person, many memberships. You are one identity on the platform, and you are a member of every organization that has invited you, with a possibly different role in each. An owner in your own company, an editor in a client's, a viewer in a third.
The invitation adds a membership without creating a person. It has to work for someone who has no account yet, and for someone who already has one and is joining another organization.
The click
An owner opens their members list, enters a teammate's email, picks the role that teammate will hold, and sends. On my server, that creates an invitation record with a securely generated random token and an expiry twenty-four hours out, and the token is the whole security model. It is a secret long enough that nobody guesses it, carried in a link, and it is the only key that opens this particular invitation.
That invitation now has to reach an inbox.
The notifications layer underneath
You can run your own outbound mail server. People do. What you take on with it is deliverability, which is the work of making inboxes accept your mail: reputation on a new sending IP, SPF and DKIM and DMARC records, bounce handling, complaint feedback loops, and the slow warming of a domain nobody has seen before. Get any of it wrong and your invitations land in spam, which looks identical to your invitations not being sent.
That is a job. It is not the job I am doing, so outbound mail goes through a transactional email service that has already solved it. SendGrid, Mailgun, Postmark, Amazon SES and Resend all do this. Any of them is a better sender than I would be.
I would rather it were Google, because everything else here runs on Google Cloud and one provider means one bill, one identity model, one place to look during an incident. Google Cloud has no transactional email service. So this is the one dependency sitting outside the platform's cloud, and it is a deliberate exception rather than an oversight.
Which vendor it is should not be a fact the product knows. Invitations are one of a dozen things the platform sends. Password resets, billing warnings, notifications. Wiring each of them to a vendor directly means changing a dozen features to change one vendor, and email is only one channel anyway: a notification can also reach a device or a badge inside the app.
So the invitation hands a message to a notifications system, and behind that sits one interface:
The provider interface
export interface EmailProvider { send(options: EmailOptions): Promise<EmailResult>;}Behind that interface sits whichever provider the environment chooses. In production, the transactional service. Locally, MailHog, a fake mail server that catches everything the app sends and shows it in a web inbox.
MailHog is how the flow got proved. I clicked invite, opened the web inbox, and there was the real email with the real link, which I clicked and accepted, and no message left my machine. It is a permanent fixture of the dev environment, because the alternative is sending test invitations to my own address all day.
The invitation code names none of these providers. It calls the notifications system, and the notifications system resolves the rest.
Who can accept it
The teammate gets an email with a link carrying the token, and clicks it. Two people can arrive at that link, and both have to work.
Someone with no account signs up, and the token is spent afterwards. The identity comes first, the membership second.
Someone who already has an account, from another organization they belong to, is added to this one. No second identity, no second password. The same person, holding one more membership.
Both arrivals end at the same check. The email on the accepting account must match the email the invitation was sent to. An invitation to maria@agency.com can only be accepted by the account that owns maria@agency.com, which closes the hole a forwarded link would otherwise open. An invite gets one person into one organization, and only the person it names.
The membership is written
The token is spent, the membership row is written, and the new member lands in the organization holding exactly the role the owner picked. Their next request crosses the same permission gates as everyone else, and now those gates know them. In the members list, the pending row becomes a person.
Wiring this up yourself
If you are building multitenant software, the invitation is the foundation of your membership model, so build it early and build it as a token flow.
Make the token the security model. A long random secret in a link, a short expiry, single use. Verify by the email the accepter proves they own rather than by anything the sender controls.
Handle both arrivals from the first line of code. Someone signing up for the first time, and someone who already has an account joining an additional organization. A flow that only handles new signups puts a wall in front of every freelancer and agency, because those are the people who join a second organization.
And when the invitation needs to email someone, build the notifications layer before wiring anything to a vendor. Provider behind an interface, invite routed through it. Password resets, billing warnings and in-app notifications all arrive later and all need the same path, and adding them to an interface that already exists is cheaper than retrofitting one around a hard-coded vendor call.
Building something like this
I'm Jesse. I build platforms end to end, and I'm open to work. If this is the kind of engineering you need, get in touch.