A gallery of the personal websites built by students of the AI Engineering & Career Accelerator by Mohammad Kabajah. Anyone can browse and add a site. Only admins can edit or delete entries.
Built with Vite, React and TypeScript, using plain CSS with custom properties. Data and admin auth run on Supabase (Postgres, Auth and Row Level Security), and the app deploys as a static site.
npm install
cp .env.example .env.local # then fill in the Supabase values
npm run devThe app needs a Supabase project (see below). Without the env vars it shows "Couldn't load the gallery".
Copy .env.example to .env.local:
| Variable | Purpose |
|---|---|
VITE_SUPABASE_URL |
Supabase project URL (Project Settings → API) |
VITE_SUPABASE_ANON_KEY |
Supabase anon (public) key. It is safe in the browser because RLS enforces every permission. |
VITE_SCREENSHOT_URL_TEMPLATE |
Screenshot provider URL. {url} is replaced with the encoded site URL. Defaults to Microlink with a 1280×800 viewport. |
Set the same variables in your host's dashboard (Netlify, Vercel, Cloudflare Pages, …). Build command: npm run build. Publish directory: dist.
-
Create a project at supabase.com. The free tier is enough.
-
Apply the schema. Either:
- with the CLI:
supabase link --project-ref <ref>and thensupabase db push, or - paste
supabase/migrations/20260929000000_create_sites.sqlinto SQL Editor and run it.
This creates the
sitesandadminstables, the validation constraints, the RLS policies and column grants, and addssitesto Realtime. - with the CLI:
-
Turn off public sign-ups: Authentication → Sign In / Providers → uncheck Allow new users to sign up. Non-admin accounts can't do anything, but there's no reason to allow them.
-
Create the first admin:
- Authentication → Users → Add user → Create new user. Enter an email and password and tick Auto confirm.
- In the SQL Editor, run:
insert into public.admins (user_id) select id from auth.users where email = 'staff@example.com';
- Repeat for each staff member. To remove an admin, delete their row from
public.admins.
Where credentials live: admin emails and passwords are stored only in Supabase Auth (
auth.users, with passwords hashed by bcrypt). They are never in this repo, in env vars or in the frontend bundle.public.adminsholds only user IDs. To change a password, use Authentication → Users → the user's menu → Send password recovery, or set a new one from the dashboard. -
Log in on the site with the pen icon in the header.
-
Reactions: apply the later migrations too (
supabase db pushapplies them all). Then turn on Authentication → Sign In / Providers → Allow anonymous sign-ins. Each visitor gets an anonymous session on their first reaction, and that session is how the site remembers which reactions are theirs. Supabase also refuses anonymous sign-ins while Allow new users to sign up is off, so if you turned it off in step 3, turn it back on. This is safe: a non-admin account can only do what an anonymous visitor can (react), and the site signs non-admins straight back out. Consider enabling CAPTCHA under Authentication → Attack Protection.
select: anyone.insert: anyone. Only the form columns can be written:id,created_atandscreenshot_urlcannot be set by visitors. Check constraints require a name of 1–80 characters after trimming, andhttp(s)://URLs of 300 characters or fewer. The hidden honeypot field must be empty.update/delete: only signed-in users listed inadmins, via theis_admin()security-definer function.admins: each user can read only their own row. It cannot be written from the client.
Each card calls getPreviewUrl(siteUrl) (in src/lib/preview.ts), which fills in VITE_SCREENSHOT_URL_TEMPLATE. If a row has a screenshot_url, that value is used instead.
- Microlink's free tier is rate-limited. For a whole class, capture each site once and store the result in
screenshot_url, for example with a Supabase Edge Function or a small script using the service-role key. You can also use a paid Microlink key or another provider. - The hover "peek" scrolls through the page when the image is a full-page capture (taller than about 4:3). Add
&screenshot.fullPage=trueto the Microlink template to get one. With viewport-only captures the card zooms in gently instead.
| Script | What it does |
|---|---|
npm run dev |
Start the dev server |
npm run build |
Type-check and build to dist/ |
npm run og |
Re-render public/og.png from scripts/og.html with headless Chrome (macOS path) |
After deploying, you may want to make og:image in index.html an absolute URL on your domain. Some link-preview scrapers ignore relative URLs.