A debate forum for religion, philosophy, and secular thought — built with real architecture decisions.
Sacred Discourse is a structured debate forum spanning four traditions — Islam, Christianity, Judaism, and Atheism & Secularism. It is a full-stack portfolio project demonstrating production-minded frontend architecture, thoughtful UX, and a backend secured entirely through database-level policies — from schema design and Row Level Security through file upload pipelines, full-text search, and a polished, animated UI.
I've spent years studying theology and comparative religion — it's genuinely one of the things I find most interesting to think about, and I wanted a space where those conversations could happen with some structure and care. Most online discussions about religion lose shape quickly, and I wanted to see what a more intentional forum could look like across Islam, Christianity, Judaism, and secular thought. Building Sacred Discourse let me work on something technically serious while solving a problem I actually care about. Longer-term, I'm hoping to keep improving the platform and eventually deploy it for the Dawah Club at my school, pending approval.
Create an account, post a discussion, reply to others, upload attachments, search across traditions, and edit your profile — the full flow is wired and live.
- Email/password sign-up and sign-in via Supabase Auth
- Email confirmation with a custom confirmation screen
- Forgot password → reset password via tokenised email link
- Password visibility toggle; session persisted across tabs and page refreshes
- Create, edit, and delete posts with title (100 chars), description (750 chars), and optional attachments
- Character counters on all fields that turn red near the limit
- Inline edit form with attachment management — add new files, remove existing ones, diff-based Storage cleanup on save
- Orphan-safe upload pipeline: if a post insert fails after upload, Storage files are deleted automatically
- View counter incremented only by authenticated non-authors
- Threaded replies up to 2,000 characters; collapsible beyond 300
- Reply form at the top of the section; smooth scroll to the bottom of the list on submit
- Post authors can delete any reply on their post; reply authors can delete their own
- Optimistic like/unlike for posts and replies — updates instantly, reverts silently on failure
- Like counts are public; toggling requires authentication
- PDF, JPEG, PNG, GIF, PPTX, DOCX — validated client-side, 50 MB per file
- Inline preview: PDFs in an
<iframe>, images in a fullscreen modal, Office docs via the Office Online viewer - Download links for all attachment types
- Full-text search using PostgreSQL
tsvectorwith a GIN index - Results grouped by tradition with per-topic accent colours
- Empty state correctly distinguishes "no results" from a query failure
- Clickable author names throughout the app navigate to public profile pages
- Avatar upload with live preview, old avatar cleanup on update, and immediate navbar sync
- Display name editing inline; post history grouped by topic
- Sticky navbar with a Topics dropdown; keyboard-accessible search (Enter with ≥ 3 characters)
- Route fade transition between pages
| Technology | Role | Why |
|---|---|---|
| React 18 | UI | Component model maps cleanly to the forum's entity hierarchy (Topic → Post → Reply). useContext for scoped state sharing without Redux overhead. |
| Vite | Build tool | Sub-second HMR during development. Trivial Vercel deployment. No configuration overhead. |
| Tailwind CSS v3 | Styling | Utility-first keeps styles co-located and eliminates CSS file sprawl. JIT handles arbitrary values without a config change. |
| React Router v6 | Routing | Nested route params (/topic/:topicSlug/post/:postId) map directly to the data hierarchy. useSearchParams handles the search flow cleanly. |
| Supabase | Auth, Database, Storage | Replaces a Node/Express backend entirely. Postgres RLS enforces data ownership at the database layer — not the application layer — so no route guard can accidentally expose data. |
| Vercel | Deployment | Zero-config for Vite projects. Preview deployments per branch. |
Every table has RLS enabled. Policies are strict: author_id = auth.uid() for writes, public read for posts, replies, likes, and profiles. The application never has to trust that a route guard ran correctly — the database rejects the operation outright if the policy fails.
This also means any new client (mobile app, CLI tool) inherits the same security model without extra work.
PostDetailPage manages 35+ state variables across four data-fetching effects, six event handlers, and four sub-components (EditPostForm, AttachmentViewer, ReplyCard, FullscreenImageModal). Prop-drilling all of this would make each component signature unreadable and tightly couple them to the parent's implementation.
The solution: a PostDetailContext created and provided inside PostDetailPage, consumed via a usePostDetail() hook. The context is not exported or accessible outside this module's graph — it is a private implementation detail, not a global concern. Sub-components are co-located in src/components/post-detail/ and cannot be imported anywhere else.
Search runs against a generated column rather than a LIKE query:
ALTER TABLE posts
ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (
to_tsvector('english', coalesce(title, '') || ' ' || coalesce(description, ''))
) STORED;
CREATE INDEX posts_search_vector_idx ON posts USING GIN (search_vector);The GIN index makes searches fast at scale. The 'english' configuration applies stemming — a search for "believing" matches posts containing "belief". The column is maintained automatically by Postgres; there is no sync job or trigger to maintain.
The upload pipeline is sequenced deliberately:
- Upload files to Storage
- Insert the post row (
.select('id').single()) - Insert
file_attachmentsrows
If step 2 fails after step 1, orphaned Storage files are deleted before the error surfaces to the user. If step 3 fails, the post exists with no attachments — a recoverable state the user can fix by editing.
Islam topic page with the discussion list, reply/view counts, and topic-coloured post cards.
Post header with author avatar, engagement stats, and the attachment list.
Inline document preview expanded alongside the reply thread.
Profile page with avatar, edit form, and post history grouped by tradition.
Full-text search results grouped by topic with tradition-coloured section headers.
- Node.js 18+
- A Supabase project
git clone https://github.com/samooda/sacred-discourse.git
cd sacred-discourse
npm installCopy the example file and fill in your own values:
cp .env.example .envThis creates a .env in the project root with the required keys:
VITE_SUPABASE_URL=your_supabase_project_url
VITE_SUPABASE_ANON_KEY=your_supabase_anon_keyBoth values are available in your Supabase project under Settings → API. The .env file is gitignored; .env.example is the committed template.
npm run devThe app will be available at http://localhost:5173.
The app uses five tables: profiles, posts, replies, likes, and file_attachments. After creating the tables and enabling RLS, two additional steps are required.
Add the search_vector generated column and GIN index to posts:
ALTER TABLE posts
ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (
to_tsvector('english', coalesce(title, '') || ' ' || coalesce(description, ''))
) STORED;
CREATE INDEX posts_search_vector_idx ON posts USING GIN (search_vector);Grant schema access to the Supabase roles:
grant usage on schema public to anon, authenticated;
grant select on all tables in schema public to anon;
grant all on all tables in schema public to authenticated;A profiles row is created automatically via a Supabase trigger on auth.users insert, seeded with the display_name from signup metadata.
- Semantic search — replace or augment
tsvectorwith embedding-based similarity search via a FastAPI microservice andpgvector, enabling conceptually related results across traditions - Real-time replies — Supabase Realtime subscriptions to push new replies to all viewers without polling
- Mobile-responsive design — the current layout is desktop-first; a responsive pass would bring the forum to smaller screens
- Google OAuth — a one-click sign-in path alongside the existing email/password flow
- Moderation tools — admin role with the ability to pin posts, lock threads, and remove content
- Pagination or infinite scroll — the current implementation loads all posts per topic in a single query; pagination would be necessary at scale
- Building security at the database layer instead of the application layer changed how I think about trust boundaries — the database rejects an unauthorized operation outright, so there is no route guard that can accidentally fail to run, and any future client inherits the same guarantees without extra work.
- Real UX edge cases — orphaned Storage files after a failed post insert, UI that needs to revert silently on a failed optimistic update, empty states that distinguish "no data" from "query failed" — exposed a whole layer of work that tutorial projects skip entirely, and getting them right took more time than the happy-path features.
- Treating this as something I actually intend to deploy forced harder decisions: I had to decide what "done enough" means for each feature and resist shipping half-finished work just to check a box.
- Scope management turned out to be as important as any technical skill — the Future Improvements section is a deliberate record of work I chose to defer, not things I ran out of time for, and drawing that line clearly kept the shipped features solid.
- If I were starting over, I would design for pagination from the first query rather than treating it as something to layer in later; it touches the data-fetching shape deeply enough that retrofitting it is more disruptive than building it in from the start.
Abdessamad Atifi
