Systems Integration & Architecture — Academic Documentation

Suot
Architecture.

System design, data model, integration points, and component overview for the Suot peer-to-peer fashion swapping platform.

9
Core Modules
11
DB Tables
5
Integrations
SDG 12
UN Goal
01 — Overview

System Architecture

Suot is a client-side single-page web application. All backend logic — authentication, database, file storage, and real-time communication — is handled by Supabase. The browser communicates directly with Supabase over HTTPS; there is no custom application server.

Client Layer — Browser (Vanilla JS ES Modules)
Login / Register
Dashboard / Catalog
Messages
Home Feed
Wallet / Top-Up
Profile / Friends
Super Admin
supabase-js v2 SDK — HTTPS REST + WebSocket
Backend Layer — Supabase (BaaS)
Auth Service
PostgreSQL DB
Realtime / Broadcast
File Storage
DB Triggers / RLS
Direct API calls (no app server)
External Services
Gemini AI API
Nominatim (OpenStreetMap)
Google OAuth
OpenRelay TURN (WebRTC)
Google Fonts / CDN
02 — Admin Layer

Super Admin System Flow & Control Layer

The Super Admin module provides centralized control over platform operations, including user management, content moderation, transaction monitoring, and system-level configuration. Unlike standard users, the admin interacts with privileged database operations enforced through Supabase Row Level Security (RLS) and role-based access control. All actions are executed directly via secured API calls to Supabase, ensuring real-time oversight without a dedicated backend server.

Super Admin Architecture Diagram
Super Admin — System Control Architecture
03 — User Journey

Core Swap Flow

The primary user journey — from discovering an item to completing a verified swap — involves 8 distinct steps across 5 pages, with the database transaction secured by a dual OTP confirmation.

1

Authentication

User registers or signs in via email/password or Google OAuth. Supabase Auth issues a JWT. A trigger auto-creates a profiles row on first sign-up. Session stored in Supabase's cookie-based storage.

auth/login.html → dashboard
2

Browse Catalog

Dashboard fetches all available items via fetchItems(). User can filter by category, sort by distance (Haversine formula against device geolocation), or browse the Home feed by hashtag.

dashboard/dashboard.html + personal/home.html
3

Post an Item

Seller fills form → images uploaded to Supabase Storage (item-images bucket) → public URLs saved in items.images[]. Meetup pin coordinates saved via Leaflet + Nominatim reverse geocoding.

personal/post-item.html → items table
4

Initiate Swap

Requester picks an offer (their item or pts) → swaps row created with status: 'pending' → swap request message auto-inserted in messages table → notification sent to item owner.

personal/item-detail.html → swaps table
5

Negotiate in Chat

Both users chat in real-time via Supabase Realtime Postgres changes. Swap offer card rendered inline. Owner can accept, decline, or counter. Accepted → status changes to otp_pending.

personal/message.html → messages + swaps
6

Video Call (Optional)

Either user initiates a WebRTC video call. Signaling (SDP offer/answer + ICE candidates) exchanged via Supabase Broadcast. No third-party service — peer-to-peer with OpenRelay TURN fallback.

WebRTC + Supabase Broadcast vc-{userId}
7

OTP Confirmation

Each party generates a 4-digit code stored in swaps.otp_requester/otp_owner. They swap codes in person, enter the other's code in the app. On match → transferSwapPts() runs → items marked swapped.

swaps table → status: 'swapped'
8

Points Settlement

Postgres trigger fn_circulation_check fires on pts update — caps active wallet at 2,500 pts, moves overflow to circulation_buffer (expires in 30 days), auto-refills if active ≤ 500. All logged to wallet_events.

profiles.pts → DB trigger → wallet_events
04 — Data Model

Entity Relationship Diagram

All data lives in Supabase PostgreSQL. Row Level Security policies ensure users can only read/write their own data. Every table uses UUIDs as primary keys generated by gen_random_uuid(). Required tables: profiles, items, swaps, messages, wishlist, follows, notifications, wallet_events, posts, post_reactions, stories.

profiles
iduuid PK
usernametext UNIQUE
display_nametext
avatar_urltext
ptsint DEFAULT 500
circulation_bufferint DEFAULT 0
buffer_expires_attimestamptz
bio, locationtext
items
iduuid PK
user_iduuid FK→profiles
name, categorytext
imagestext[]
ptsint
condition, sizetext
latitude, longitudefloat8
statustext DEFAULT 'available'
swaps
iduuid PK
requester_iduuid FK→profiles
owner_iduuid FK→profiles
requested_item_iduuid FK→items
offered_item_iduuid FK→items NULL
offered_ptsint DEFAULT 0
statustext
otp_requester, otp_ownertext
messages
iduuid PK
from_user_iduuid FK→profiles
to_user_iduuid FK→profiles
bodytext
msg_typetext
item_id, swap_iduuid FK NULL
reactionsjsonb DEFAULT {}
readbool DEFAULT false
wallet_events
iduuid PK
user_iduuid FK→profiles
event_typetext (CHECK)
amountint
from_wallettext
to_wallettext
notetext
notifications
iduuid PK
user_iduuid FK→profiles
typetext
messagetext
linktext
thumbnail_urltext
readbool DEFAULT false
wishlist
iduuid PK
user_iduuid FK→profiles
item_iduuid FK→items
UNIQUE(user_id, item_id)constraint
follows
iduuid PK
follower_iduuid FK→profiles
following_iduuid FK→profiles
UNIQUE(follower_id, following_id)constraint
posts
iduuid PK
user_iduuid FK→profiles
captiontext
imagestext[]
hashtagstext[]
linked_item_iduuid FK→items NULL
post_reactions
iduuid PK
post_iduuid FK→posts
user_iduuid FK→profiles
reaction_typetext (CHECK)
UNIQUE(post_id, user_id)constraint
stories
iduuid PK
user_iduuid FK→profiles
image_urltext
expires_attimestamptz
viewersuuid[]

Key Relationships

profilesitems1 : many (owns)
profilesswaps1 : many (as requester OR owner)
itemsswaps1 : many (requested_item OR offered_item)
profilesmessages1 : many (from_user OR to_user)
profileswallet_events1 : many (all financial movements)
profilesprofilesmany : many via follows
postspost_reactions1 : many (one reaction type per user per post)
05 — Components

Module Structure

Suot is organized into five directories under src/, plus a top-level docs/ folder at the repository root for SQL schemas and documentation. Each page is a self-contained HTML file with an inline or imported ES module script. All database calls are centralized in db/supabase.js.

Suot/ ← repository root
  ├──  docs/ ← SQL schemas & this document (root-level)
  ├──  src/
  │   ├── auth/
  │   ├── dashboard/  + admin-dashboard.html
  │   ├── personal/
  │   ├── profile/
  │   └── db/
  ├── .gitignore
  └── README.md
auth/
htmllogin.htmlSign in / Sign up
jsscript.jsAuth logic, Google OAuth
cssstyle.cssLanding page styles
dashboard/
newadmin-dashboard.htmlSuper admin panel
htmldashboard.htmlCatalog + filters
jsdashboard.jsItem loading, geo sort
cssdashboard.cssShared layout
htmltopup.html3-step Pasa-Points top-up
jstopup.jsTop-up flow logic
csstopup.cssTop-up stylesheet
htmlwallet.htmlWallet + history
jswallet.jsPasa-Points & buffer logic
csswallet.cssWallet stylesheet
personal/
htmlhome.htmlFeed + stories
htmlmessage.htmlChat + video call
htmlpost-item.htmlList item + map
htmlitem-detail.htmlItem page + swap
htmlwishlist.htmlSaved items
htmlfriends.htmlFollow system
jsconfig.jsGemini API key (gitignored)
profile/
htmlprofile.htmlUser profile view
htmledit-profile.htmlEdit info + avatar
db/
jssupabase.jsClient init + all DB helper functions
docs/ (repo root)
sqlsupabase-schema.sqlCore tables
sqlhome-feed-v2.sqlFeed tables
sqlwallet-circulation.sqlWallet trigger
htmlarchitecture.htmlThis document
imgsuper_admin.pngAdmin arch diagram
06 — External Systems

Systems Integration

Suot integrates with five external services. All calls are made directly from the browser (no proxy server). Sensitive keys are kept in config.js which is excluded from version control.

ServicePurposeProtocolAuthCost
SupabaseAuth, PostgreSQL database, file storage, realtime messagingHTTPS REST WebSocketAnon Key + JWTFree tier
Google GeminiAI-powered Pasa-Points price suggestion for listed itemsHTTPS RESTAPI Key (config.js)Free tier
OpenStreetMap / NominatimForward & reverse geocoding for meetup location pinsHTTPS RESTNone (rate limited)Free
Google OAuthSocial sign-in — users can register/log in with their Google accountOAuth 2.0Client ID via SupabaseFree
OpenRelay TURNTURN relay fallback for WebRTC video calls on restricted networksTURN/UDP+TCPUsername/credentialFree
Google FontsWeb font delivery — Playfair Display, Inter, Great Vibes, DM SansHTTPS CDNNoneFree
07 — Video Calling

WebRTC Call Sequence

Video calls use native browser WebRTC — no third-party video service or meeting codes. Supabase Realtime Broadcast acts as the signaling channel to exchange SDP and ICE candidates between peers.

Caller Browser
Supabase Broadcast
Callee Browser
STUN / TURN
① SIGNALING PHASE

Caller: getUserMedia() + createOffer()

Browser requests camera/mic permission. RTCPeerConnection created with ICE servers. SDP offer generated and set as local description.

sendSignal(calleeId, 'incoming_call', {offer, callerInfo})

SDP offer embedded in the broadcast payload. Supabase Realtime delivers it to the callee's subscribed vc-{calleeId} channel. No REST fallback — channel awaits SUBSCRIBED state before send.

Supabase Broadcast → vc-{calleeId}

Callee: getUserMedia() + setRemoteDescription(offer) + createAnswer()

Callee accepts call, gets camera/mic, applies the offer as remote description, generates SDP answer.

sendSignal(callerId, 'call_accepted', {answer})

SDP answer sent back to caller. Caller sets it as remote description.

Supabase Broadcast → vc-{callerId}
② ICE NEGOTIATION

ICE candidates exchanged via Supabase Broadcast

Each browser's onicecandidate fires and sends candidates to the peer. Candidates queued in iceCandidateQueue[] if remote description isn't set yet, then flushed.

STUN: discovers public IP — TURN: relays if P2P blocked
③ MEDIA STREAM

ontrack fires → remoteVideo.srcObject = stream

Peer-to-peer audio/video flows directly between browsers (or via TURN relay). "Connecting…" overlay hidden. Call timer starts.

④ CALL END

sendSignal(peerId, 'call_ended', {duration})

Duration passed in payload. Both sides render an inline chat message: "Video call · 2m 34s". All media tracks stopped. RTCPeerConnection closed.

08 — Business Logic

Wallet Circulation System

Points management is enforced entirely in the database via a PostgreSQL BEFORE UPDATE trigger. No client-side enforcement — the rule fires regardless of which page updates pts.

Wallet Rules
Active cap2,500 pts
Overflow triggerpts > 2500
Overflow actionexcess → buffer
Refill triggerpts ≤ 500
Refill amounttop up to 2500
Buffer expiry30 days
wallet_events types
topupUser topped up via payment method
overflowExcess moved to buffer automatically
refillBuffer refilled active wallet
spendPoints spent on swap request
earnPoints earned from swap completion
adminManual adjustment
fn_circulation_check() — Postgres Trigger
BEFORE UPDATE OF pts ON profiles
  IF NEW.pts > 2500 THEN
    excess := NEW.pts - 2500
    NEW.pts := 2500
    NEW.circulation_buffer += excess
    NEW.buffer_expires_at := NOW() + INTERVAL '30 days'
    INSERT INTO wallet_events (overflow, excess) ...

  ELSIF NEW.pts <= 500 AND NEW.circulation_buffer > 0 THEN
    refill := LEAST(2500 - NEW.pts, NEW.circulation_buffer)
    NEW.pts += refill
    NEW.circulation_buffer -= refill
    INSERT INTO wallet_events (refill, refill) ...
09 — Stack Summary

Technology Stack

Every technology choice prioritizes zero monthly cost and zero infrastructure management — appropriate for a student project that must remain live for grading.

HTML5 + CSS3
Vanilla JS ES Modules
Supabase (BaaS)
PostgreSQL
Leaflet.js + OpenStreetMap
Google Gemini AI
WebRTC (native)
Google OAuth 2.0
OpenRelay TURN