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, PDF downloads, and no database anywhere.

H

Hans Martens

2 min read

Clients used to email me asking for a copy of an invoice they’d misplaced. Never a big deal on its own — but a steady little interruption: each time, I’d stop what I was doing, find the PDF, and send it over.

My answer to that email 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. You can even look at the sign-in page yourself — without a registered email address it politely goes nowhere.

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.
  • 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.

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, the API key was already in place; I documented that whole setup in Contact Form Setup: Resend, Vercel, and GoDaddy Step by Step.

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 pulls that client’s invoices and projects live from the Moneybird API as it renders.
  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: 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 system has no state of its own to manage, so there’s nothing for me to maintain beyond the site I was already running.

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. Have a look at what I offer, or tell me what your clients keep emailing you for.

Share:

Related Posts

How I Built a Tech-Stack Marquee as an Astro Developer

As a freelance Astro developer, I built a brand-coloured, pure-CSS tech-stack marquee for my homepage hero and About page — and shipped the same component in my Astro Rocket theme.

HHans Martens
2 min read
#astro#astro-developer#web-development#astro-rocket

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
2 min read
#astro#wordpress#cms#performance#web-development#comparison

Why I Use Claude Code for the Work I Do

Why Claude Code — running on Opus 4.8 — has become the coding partner I reach for on every Astro build. What it is, how I actually use it, and where it earns its keep.

HHans Martens
2 min read
#claude-code#ai#opus-4-8#workflow#astro#web-development

Follow along

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