Dealyly — System Flowchart

Dealyly · System reference

How Dealyly works, end‑to‑end

A map of the whole platform — the clients, the API, the data stores, and the seven flows that make up the product — written so QA can see how every piece connects and where the states worth testing live.

Prepared for Shehryar (QA) Scope backend · web · mobile Market Algeria · fr / ar / en / kab Status pre‑launch
Backend
dealyly-backend
Fastify · TypeScript · PostgreSQL (Drizzle) · Redis · AWS ECS Fargate (eu‑west‑3)
Web
dealyly-web
Next.js 15 · React 19 · Tailwind · CloudFront + Lambda (OpenNext). Includes the admin panel.
Mobile
dealyly-mobile
React Native 0.81 · Expo · iOS + Android. Talks to the same API.
01

System architecture

Three clients talk to one Fastify API. The web app is server-rendered at the edge (CloudFront + Lambda); the mobile app calls the API directly. The API owns all business logic and is the only thing that touches the database, cache, media store, and third-party services.

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#FFF3EC','primaryBorderColor':'#EE8A54','primaryTextColor':'#1C1917','lineColor':'#B0A99F','secondaryColor':'#EEF3F0','tertiaryColor':'#F3F1ED','clusterBkg':'#FBFAF8','clusterBorder':'#E1DCD4'}}}%%
flowchart TB
  subgraph CL["Clients"]
    W["Web app
Next.js"] M["Mobile app
React Native"] AD["Admin panel
/admin (in web)"] end subgraph EDGE["AWS edge"] CF["CloudFront CDN"] LM["Lambda
SSR + media"] end API["Fastify API
REST + WebSocket
ECS Fargate"] subgraph DATA["Data"] PG[("PostgreSQL
RDS")] RD[("Redis
cache · rate-limit")] S3[("S3
media")] end subgraph EXT["External services"] SAT["SATIM
CIB payments"] TS["Cloudflare
Turnstile"] OA["Google · Apple
OAuth"] PUSH["FCM · APNs
push"] SES["AWS SES
email"] end W --> CF --> LM --> API AD --> CF M --> API API --> PG API --> RD API --> S3 S3 --> CF W -. captcha .-> TS API --> SAT API --> OA API --> PUSH API --> SES
service / component data store boundary (owner)

02

Authentication & sessions

Users sign up with email + password or with Google / Apple. The API issues a short-lived access token (JWT) plus a refresh token. Admins must enrol in 2FA (TOTP), and sensitive money / admin actions demand a fresh 2FA "step-up". Logging out or resetting a password invalidates existing access tokens.

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#FFF3EC','primaryBorderColor':'#EE8A54','primaryTextColor':'#1C1917','lineColor':'#B0A99F','tertiaryColor':'#F3F1ED','clusterBkg':'#FBFAF8','clusterBorder':'#E1DCD4'}}}%%
flowchart TB
  start(["Visitor"]) --> choice{"Sign up or log in?"}
  choice -->|"Email"| pw["Email + password"]
  choice -->|"Social"| oauth["Google / Apple OAuth"]
  pw --> verify["Verify email
(link)"] oauth --> link["Link / create
oauth_identities"] verify --> issue["Issue JWT access (7d)
+ refresh token"] link --> issue issue --> role{"Admin?"} role -->|"No"| user["Authenticated user"] role -->|"Yes"| twofa["2FA enrolment
required (TOTP)"] twofa --> admin["Admin session"] admin --> stepup{"Sensitive action?
money · RBAC · KYC"} stepup -->|"Yes"| fresh["Require fresh 2FA
step-up"] fresh --> act["Action allowed"] user --> logout["Logout / password reset"] logout --> revoke["Access tokens invalidated
(tokens_invalid_before)"]
QA focus
  • Token boundaries: a 2FA-challenge token must never act as a full session; an old access token must stop working after logout / password reset.
  • Step-up: refund, dispute, RBAC grants and org KYC all require a fresh 2FA, not just enrolment.
  • Social: same email via password and via Google should resolve to one account, not two.

03

Listing lifecycle

A seller composes a listing, uploads photos (which flow through the media pipeline), and submits. Content passes moderation — banned-word and image checks, with an AI-flag queue for review — before it goes live and becomes searchable. Buyers then discover it and contact the seller.

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#FFF3EC','primaryBorderColor':'#EE8A54','primaryTextColor':'#1C1917','lineColor':'#B0A99F','tertiaryColor':'#F3F1ED'}}}%%
flowchart LR
  compose["Seller composes
(deposer)"] --> photos["Upload photos
→ media pipeline"] photos --> submit["Submit listing"] submit --> mod{"Moderation
banned words · image · AI flag"} mod -->|"Flagged"| queue["Admin review queue"] queue -->|"Approve"| live queue -->|"Reject"| rejected["Rejected
seller notified"] mod -->|"Clean"| live["Published · live"] live --> index["Indexed for search
+ saved-search alerts"] index --> view["Buyer views listing"] view --> contact["Contact seller
chat / call"] live --> boost["Optional: boost
→ Payments (§5)"]
QA focus
  • States: draft → pending → published → rejected / expired / removed — each should render and behave correctly on web + mobile.
  • Moderation: banned words block; an AI-flagged item lands in the admin queue, not live.
  • 69 wilayas (not 58) everywhere a location is shown or filtered.

04

Search & messaging

Discovery is faceted search with relevance ranking and keyset pagination; buyers can save a search to be alerted on new matches. Contacting a seller opens a conversation — text, images and documents — and delivers a push notification that deep-links straight into the chat.

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#FFF3EC','primaryBorderColor':'#EE8A54','primaryTextColor':'#1C1917','lineColor':'#B0A99F','tertiaryColor':'#F3F1ED','clusterBkg':'#FBFAF8','clusterBorder':'#E1DCD4'}}}%%
flowchart TB
  subgraph SRCH["Search & discovery"]
    q["Query + facets
category · wilaya · price · features"] --> eng["Search engine
relevance · keyset pagination"] eng --> res["Results"] res --> save["Save search"] save --> alert["New match →
saved-search alert (email)"] end subgraph MSG["Messaging"] open["Buyer contacts seller"] --> conv["Conversation"] conv --> msg["Messages
text · image · document"] msg --> push["Push notification
FCM / APNs"] push --> deep["Deep link → chat
(web + mobile)"] end res --> open
QA focus
  • Pagination: "load more" must keep the active filters (no stale-filter bleed) and never skip or duplicate rows.
  • Saved-search alerts fire only on genuinely new matches after the save.
  • Deep links: a chat push opens the correct conversation from a cold start on iOS + Android.

05

Payments & monetization

Paid products (listing boosts, shop boosts, photo packs, subscriptions) go into a cart. Online card payment runs through SATIM (Algeria's CIB gateway): a dedicated pay page collects consent + an anti-bot check, the server registers the order and redirects to SATIM's hosted card page, then confirms the outcome server-side before granting anything. A manual bank-transfer path is the fallback. Every paid cart produces a fiscal invoice (facture) and a receipt.

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#FFF3EC','primaryBorderColor':'#EE8A54','primaryTextColor':'#1C1917','lineColor':'#B0A99F','tertiaryColor':'#F3F1ED','clusterBkg':'#FBFAF8','clusterBorder':'#E1DCD4'}}}%%
flowchart TB
  buy["Buy boost / subscription / photo pack"] --> cart["Cart (pending)"]
  cart --> mode{"SATIM online enabled?"}
  mode -->|"No"| manual["Manual bank transfer
instructions"] manual --> adminpaid["Admin marks paid"] mode -->|"Yes"| pay["Pay page
amount · CGV consent · Turnstile · CIB button"] pay --> reg["checkout-session
verify captcha → register with SATIM"] reg --> hosted["Redirect to SATIM
hosted card page"] hosted --> ret["Return to site"] ret --> confirm["Server confirm
acknowledgeTransaction"] confirm --> ok{"Accepted?
OrderStatus 2"} ok -->|"Yes"| grant["Grant benefit
markCartPaid"] ok -->|"No"| reject["Rejected page
reason + SATIM 3020"] adminpaid --> grant grant --> inv["Invoice (facture)
+ receipt: print / PDF / email"] reg -. closed tab .-> recon["Reconcile job
(safety net)"] recon --> confirm
QA focus
  • Grant only on success: only an accepted result (OrderStatus 2) grants; every declined test card must refuse with no benefit and no invoice.
  • Server-truth: the outcome and amount come from the server confirm, never the browser redirect.
  • Receipt (SATIM cert): accepted page shows status, transaction id, order ref, approval code, CIB, date+time, amount, and the 3020 line; print / download-PDF / email all work.
  • Money units: cart/pricing are whole DZD; invoices/ledger are centimes — the ×100 boundary must hold.

06

Admin & moderation

The admin panel lives inside the web app under /admin, gated server-side by role and fine-grained permissions (a legacy super-admin holds a * wildcard). Staff moderate listings and media, resolve user reports (which can suspend a listing or block a user), review fraud signals, run finance, and handle organization KYC — with a 2FA step-up on the dangerous actions.

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#FFF3EC','primaryBorderColor':'#EE8A54','primaryTextColor':'#1C1917','lineColor':'#B0A99F','tertiaryColor':'#F3F1ED','clusterBkg':'#FBFAF8','clusterBorder':'#E1DCD4'}}}%%
flowchart TB
  admin["Admin / staff"] --> gate["/admin — role + permission gate
2FA enrolled"] gate --> dash["Dashboards
KPIs · revenue · funnels"] gate --> modq["Moderation queue
listings · media · banned words"] gate --> reports["Reports → resolve
suspend listing / block user"] gate --> fraud["Fraud signals
repeat offenders · velocity"] gate --> fin["Finance
carts · payments · invoices · leads"] gate --> orgs["Organizations
KYC verify / reject"] reports -. dangerous .-> su["2FA step-up"] fin -. money .-> su orgs -. KYC .-> su
QA focus
  • Authorization is two-source: users.role (legacy) and access-groups — don't judge access from the groups UI alone.
  • Report actions really act: "listing removed" suspends the listing; "user blocked" blocks the user + seller.
  • Every dangerous mutation is gated on its own permission and a fresh 2FA.

07

Infrastructure & deployment

Each repo deploys from main via GitHub Actions. The backend runs DB migrations as a one-shot, then rolls the ECS service; the web deploys to CloudFront + Lambda with a blocking post-deploy contract test. Media is served signed through CloudFront to a Lambda transform. Mobile ships as native builds (Android APK/AAB, iOS TestFlight).

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#FFF3EC','primaryBorderColor':'#EE8A54','primaryTextColor':'#1C1917','lineColor':'#B0A99F','tertiaryColor':'#F3F1ED','clusterBkg':'#FBFAF8','clusterBorder':'#E1DCD4'}}}%%
flowchart LR
  subgraph CI["push to main → GitHub Actions"]
    be["backend"] --> mig["Migrate DB
(one-shot)"] --> ecs["Roll ECS service"] web["web"] --> onx["Build OpenNext"] --> cfl["CloudFront + Lambda"] --> ctest["Contract test
(blocking)"] mob["mobile"] --> nat["Native build
Android APK/AAB · iOS TestFlight"] end subgraph MEDIA["Media serving"] up["Upload → S3"] --> sign["Signed media.dealyly.com/t/"] --> tr["CloudFront → Lambda transform
resize · webp"] end ecs --> run["Prod API"] cfl --> run2["Prod web"]
QA focus
  • Migrations: a new SQL migration must be journalled with a strictly-increasing when, or it is silently skipped.
  • Web gate: the blocking contract test must pass post-deploy (it has caught site-wide 500s before).
  • Mobile depends on backend: a mobile change that needs a new endpoint ships only after that endpoint is live.