Skip to content
HHans Martens Dev

How I Built a Passwordless Client Portal with Astro and Moneybird

How I built a passwordless client portal with Astro, Moneybird and Resend — magic-link sign-in, live invoices, project status from Markdown files, and no database anywhere.

H

Hans Martens

8 min read

When a client needs a copy of an invoice, the answer is now a Login button. Clients of mine sign in to a portal on this site — no password, just a link — and see their invoices exactly as they stand in my bookkeeping: what’s open, what’s paid, what’s overdue, each one downloadable as the official PDF. I wrote up the architecture briefly on the project page for this site; this post is the longer story of how it works and why it’s built the way it is.

What clients see (and download)

Signing in lands on a dashboard with three things a client actually cares about:

  • Outstanding balance — one number, straight from the books.
  • Active projects — what I’m currently building for them, each with a phase tracker, a dated update timeline, a “waiting on you” checklist, and a change-request form.
  • Recent invoices — with a full list one click away.

Every invoice shows its live status — open, paid, late — and offers the official PDF for download. That’s deliberately all there is. No notifications, no messaging, no “engagement”. A client visits, finds the document they need, and leaves. Draft invoices never appear, and every request checks that the invoice actually belongs to the signed-in client before anything is returned.

The part that matters: none of this is a copy. The portal renders whatever my bookkeeping says at that moment, so it can never show a stale balance or a missing invoice.

There isn’t a single password in this system. Not hashed, not salted, not forgotten — just absent.

A client types the email address their invoices are sent to. A server endpoint checks it against the contacts in my bookkeeping, and if it matches, mails out a sign-in link that’s valid for fifteen minutes. Clicking the link starts a thirty-day session, carried in a signed, httpOnly cookie. That’s the entire ceremony.

Two quiet details I care about:

  • The form responds identically whether an address is registered or not, so it can’t be used to probe who my clients are.
  • A hidden honeypot field swallows the spam bots, the same trick my contact form uses.

The link tokens themselves are stateless — signed with a server-side secret rather than stored anywhere — which is also why the portal needs no user table. There are no accounts to create, no records to clean up. A client is their entry in my bookkeeping.

Moneybird is the database

This is the decision the whole build hangs on. My administration lives in Moneybird, the Dutch bookkeeping platform — every contact, every invoice, every payment status. So the portal doesn’t keep its own records at all: it asks the Moneybird API for the current state of a client’s invoices every time a page renders.

Nothing is synced, cached into a database, or duplicated. That means:

  • An invoice marked paid in Moneybird is paid in the portal, instantly.
  • The PDF a client downloads is streamed through the server straight out of Moneybird — my API token never reaches the browser, and no public URL to any document ever exists.
  • The API token is read-only and scoped to sales invoices alone. Even in the worst case, it could only ever look at invoices — never touch the books.

If you’re a freelancer reading this with your administration in Moneybird: this is the part to steal. You almost certainly don’t need a database for a client portal. Your bookkeeping already is one.

The projects are Markdown files

The invoice half of the portal reads from Moneybird; the project half reads from something even simpler: Markdown files in the site’s own repository. Each client project is one file — its current phase, a dated update timeline, a checklist of what I’m waiting on from the client, quick links to staging and design files. The portal renders whatever those files say, shown only to the client whose email is listed on the project.

That gives project pages the same property the invoices have — no database, nothing to sync, nothing to go stale — with git as the system of record. Updating a project is a one-minute edit: add a line to the updates, tick off what the client delivered, bump the phase when one completes, push. The deploy is the publish.

From the client’s side, that file renders as the answers to the three questions every project generates: a phase tracker shows where we are, the “waiting on you” list shows what the project needs from them, and the update timeline shows what’s been happening. The one place the portal accepts input is the change-request form on each project — and true to the architecture, it stores nothing: it sends an email, with the verified client and project attached.

Resend was already on the payroll

The magic-link emails are delivered by Resend — and here the portal got something for free, because my contact form already runs on it. The domain was verified, the DNS records were set, and the API key was already in place.

So the portal’s email needs came down to one more template on infrastructure that existed. If you’re building this fresh, do the contact form first — it’s the same work, and the portal rides along afterwards.

Three environment variables on Vercel

The portal’s entire configuration is three environment variables in the Vercel dashboard:

  1. MONEYBIRD_API_TOKEN — the read-only personal access token, created in Moneybird with only the sales-invoices scope ticked.
  2. MONEYBIRD_ADMINISTRATION_ID — which administration to read; it’s the long number in your Moneybird URL.
  3. PORTAL_SESSION_SECRET — a long random string that signs every sign-in link and session cookie. Rotate it and every session in existence is invalid within one deploy: the emergency brake.

(The fourth variable involved, RESEND_API_KEY, was already there for the contact form.)

And my favourite behaviour of the build: without those variables, the portal doesn’t break — it demos. While I was still waiting on the bookkeeping side, the very same pages served a fictional client with fictional invoices behind a one-click sign-in, clearly labelled as demo data. The day Moneybird was ready, I added the three variables and redeployed. The demo retired itself; real sign-in took over. Going live was a settings change, not a release.

How it all fits together

End to end, a sign-in travels like this:

  1. A client clicks Login in the header and lands on the sign-in page.
  2. They enter their invoice email; an Astro server endpoint on Vercel matches it against my Moneybird contacts.
  3. Resend delivers a signed link — valid for fifteen minutes, useless afterwards.
  4. The link sets a signed, httpOnly cookie: a thirty-day session, no account record created anywhere.
  5. From then on, middleware guards every portal route, and each page renders that client’s invoices live from the Moneybird API and their projects from the Markdown files in the repo.
  6. PDFs stream through the same guarded channel, straight out of the bookkeeping.

Astro renders it, Vercel runs it, Resend delivers it, Moneybird remembers it. The site itself stores nothing at all.

If you build one: four things to hold on to

  • Render the portal server-side. These pages are per-client and per-moment; this is exactly what Astro’s on-demand rendering is for, while the rest of the site stays static.
  • Keep the API token read-only and narrowly scoped. The portal only ever needs to look.
  • Answer the login form neutrally. “If this address is registered, a link is on its way” — for every address, every time.
  • Keep search engines out. Portal pages carry noindex and the routes are disallowed in robots.txt. A client portal has no business in a search result.

How I use it day to day

The honest answer: for the invoice side, I mostly don’t — and that’s the point. I send invoices from Moneybird exactly as before. Adding a new client to the portal isn’t a feature, a migration, or a deploy: the moment someone exists as a contact in my bookkeeping, the portal works for them.

The project side asks a minute of me per meaningful event: a dated line in the updates, a checkbox ticked when something arrives, the phase bumped when one completes — one file edit, one push. In return, nobody has to ask me where things stand, and I never have to write the same status email twice.

Clients get the calmer half. Instead of asking me for a copy and waiting on my inbox, they sign in and take what they need — at 23:00 on a Sunday if that’s when their quarterly VAT return happens to be due.

Want one for your business?

This portal is the kind of work I do for clients: small, fast, properly built web software that removes a recurring annoyance. If your bookkeeping lives in Moneybird it slots in especially neatly — but the pattern works with anything that has an API.

Want a portal like this for your own business or your own clients? Send me a message via the contact form or a quick WhatsApp message — I’ll walk you through my own portal in the first conversation, which is free, like always.

Share:

Related Posts

Hi, I'm Hans Martens — Freelance Astro Developer & Creator of Astro Rocket

The longer story of Hans Martens — freelance Astro developer in Veghel: 16 self-taught years of building websites, from WordPress to Astro, and the open-source Astro Rocket theme.

HHans Martens
8 min read
#about#introduction#freelance#astro-developer#astro-rocket#web-designer#veghel#astro

Why I Built My Own Astro Theme

How building my own website turned into Astro Rocket — a free, open-source Astro theme in the official Astro directory — and what that says about how I build.

HHans Martens
6 min read
#astro-rocket#astro#open-source#web-development

Astro vs WordPress: Which Should Your Website Use?

Astro vs WordPress, compared honestly — performance, security, maintenance, editing, and cost. When a fresh Astro build wins, and when a CMS is still the right call.

HHans Martens
8 min read
#astro#wordpress#cms#performance#web-development#comparison

Follow along

Stay in the loop — new articles, thoughts, and updates.