PROJECT · FULL-STACK WEB
simpleCRM
A lightweight CRM for a student club running outreach to companies for
plant tours and sponsorships. Tracks companies, the people at them, every
touch, and what you owe them next — built for about five users, entirely
on free tiers, with access rules enforced in the database rather than in
application code.
01
Why it exists
A club chasing company tours and sponsorships runs the same problem a sales
team does — who have we contacted, who replied, what did we promise, what is
going cold — but with none of the budget and a membership that turns over
every year. Commercial CRMs are priced per seat and assume the team
persists.
simpleCRM is sized for that: about five people, free tiers end to end, and
documentation written on the assumption that whoever inherits it has never
seen the code. The handover problem was treated as a design constraint, not
an afterthought.
02
Screens
[ dashboard screenshot ]
DASHBOARDOutreach goal progress, what is going cold, and the week ahead.
[ pipeline screenshot ]
PIPELINEDraggable board with an amount and close date behind a weighted funnel.
[ map screenshot ]
MAPGeocoded companies clustered by location, for planning trips.
[ company page screenshot ]
COMPANYContacts as a reporting tree, activity, files, capability tags.
03
What it does
PIPELINE
Companies move across a draggable board. Each carries an amount and a
close date, which feed a weighted funnel — so the number at the top
reflects probability, not wishful thinking.
CONTACTS AS A TREE
People are stored with their reporting lines intact rather than as a
flat list, so it is obvious who sits above the person who has gone
quiet. Deal roles sit on the contact.
MAP & CALENDAR
Geocoded companies on a clustered Leaflet map for planning trips, and a
calendar for tours and events.
COLD EMAIL DRAFTS
Drafts generated from templates the club edits itself. The app never
sends mail on anyone's behalf — it hands you text.
ACTIVITY FEED
Every action anybody takes lands in one feed, logged by database trigger
— so a change made directly in the SQL editor is recorded exactly like
one made in the app.
DELETION QUEUE
Deletes are narrower than writes. Any member can edit; only the record's
creator or an admin can delete, and company deletions go through an
approval queue with a permanent record.
IMPORT / EXPORT
CSV import for companies and contacts, bulk geocoding, and export back
out — so the club's data is never trapped in the app.
04
Security model
The interesting constraint was that volunteers would be editing this, so
application code could not be the thing standing between a bug and the
club's contact list. Authorisation lives in Postgres instead.
ROW-LEVEL SECURITY
Every policy requires a profile row, so access is decided by Postgres. A
bug in a page cannot leak data that RLS would not have handed over.
NO SERVICE-ROLE KEY
The app only ever holds the anon key, which grants nothing on its own.
There is no credential in the repository that can bypass RLS.
MAGIC LINKS ONLY
No passwords are stored, so there are no passwords to leak. An address
must be on an allowlist before a profile exists for it.
THREE ROLES
Viewer reads and writes nothing, member reads and writes, admin also
manages people. Reads go through has_access()
and writes through is_member() — hidden
buttons are only a courtesy.
05
Built with
Next.js 15
React 19
TypeScript
Supabase / Postgres
Tailwind
Leaflet
Vercel
App Router with server actions, Supabase for Postgres and magic-link auth,
Leaflet with marker clustering for the map, and Papa Parse for CSV import.
Free tier throughout — 500 MB of Postgres holds hundreds of thousands of
contacts, which the club will not outgrow.