PinoyBH
A community marketplace, job board and events page for Filipinos in Bahrain.
Members post for themselves (no businesses, agencies or recruiters): things to sell or give away, bedspaces, balikbayan box space, jobs, and community events. The full product spec is in SPEC.md.
How it's built
- Next.js 15 (App Router, TypeScript, Tailwind CSS 4), hosted on Vercel.
- Supabase: Postgres database, logins, and photo storage.
- The trust & safety rules live in the database, not only in the app: the sign-up checkbox, post limits, words that send a post to review, contact details hidden from visitors, private follower lists, auto-hide after reports, bans, and the Verified Kabayan badge. They hold even if someone talks to Supabase directly.
supabase/migrations/ database schema, rules and permissions
supabase/tests/ 119 tests for the trust & safety rules (npm run test:db)
src/app/ pages and server actions
src/components/ shared UI
src/content/rules.ts the community rules text
Set it up
The full click-by-click guide, including how to test it afterwards, is in docs/launch-guide.md. Short version:
- Supabase: create a project (keep the Data API on). Run both files in
supabase/migrations/ in the SQL Editor, in order. Set the Site URL and the redirect URL https://YOUR-SITE/**. Keep "Confirm email" on.
- Email: set up custom SMTP (e.g. Resend with your own domain). Supabase's built-in email only reaches your own team, so without this nobody else can confirm their account or reset a password. Then paste the templates from docs/email-templates.md.
- Run locally:
cp .env.example .env.local, fill it in (Supabase > Connect, or Settings > API Keys: Project URL and publishable key), npm install, npm run dev.
- First admin: sign up, then in the SQL Editor:
update public.profiles set role = 'admin'
where id = (select id from auth.users where email = '[email protected]');
Use 'moderator' for helpers. Only an admin can ban a moderator.
- Deploy: import the repo into Vercel, add the same environment variables, deploy.
- CAPTCHA: create a Cloudflare Turnstile widget, put its secret key in Supabase (Authentication > Attack Protection), then add
NEXT_PUBLIC_TURNSTILE_SITE_KEY in Vercel and redeploy. Do both together: CAPTCHA on in Supabase without the site key in Vercel blocks every sign-up and login.
Tests
npm run test:db # trust & safety rules against a throwaway Postgres (needs Postgres installed),
# run twice: as a new Supabase project and as an older one
npm run typecheck
npm run build
CI runs all three on every push.
Before going public
These block launch, not building:
- Legal review of the job and marketplace rules (SPEC.md sections 6, 7 and 14) by a Bahrain lawyer or LMRA, especially household hiring and the ban on side gigs.
- Privacy Policy (
/privacy) checked against Bahrain's PDPL (Law No. 30 of 2018), then switched back on: it is hidden for now (SHOW_PRIVACY in src/lib/constants.ts).
- Supabase paid plan (about $25/month). Free projects pause after a week without traffic.
- CAPTCHA on sign-up, login and password reset (step 6 above).
- Custom SMTP so confirmation and password emails reach members (step 2 above).
- A moderator who clears the queues every day. Posts that mention fees or visas wait for approval, so an ignored queue means real members' posts never go live.
Known limits in v1
- Notifications are in the site only (bell icon). No email yet.
- The visit counter (Admin > Overview) counts a visit per browser per day. Your own visits count too, and someone can inflate it on purpose. Good for trends, not for exact numbers.
- Comments block phone numbers, emails and links (anyone can read comments, so contact details belong behind the Contact button). Some honest comments with long numbers, like a 7-digit reference, will be refused.
- Words like "visa" and "agent" will also flag honest posts ("visa provided by employer"). That's on purpose: a moderator looks before anything about visas goes live. Tune the list under Admin > Search & flag words.
- A member who uploads photos and then abandons the form leaves those photos in storage.
- If following fails (200-follow limit, or the other member blocked you), the button simply doesn't change.
- Blocking hides your posts from that member only while they are logged in. Logged out, anyone can see live posts.