BorderReady
BorderReady, 영국에서 독일·프랑스·네덜란으로 팔레트 운송을 예약합니다
SpazaLink는 타운십 스파자 상점을 검증된 도매업체와 연결하고 재고를 추적해 거래 이력을 금융 건전성 보고서로 전환한다
SpazaLink links township spaza shops to verified wholesalers, tracks their stock, and turns their trading history into a Financial Health Report they can take to a bank.
커머스TypeScript · JavaScriptGitHub @danielsnyashadzashe-boop온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 README에서 읽을 수 있습니다. 저장소 README ↗
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Full shelves. Safe stock. Bank ready.
SpazaLink is a B2B web app for South African township spaza shops. It links shops to verified wholesalers, tracks their stock, and turns their trading history into a Financial Health Report they can take to a bank.
The idea comes from Rethabile Madonsela (founder and idea owner).
It's a live app that processes real user data against MongoDB Atlas, with sign-in handled by Clerk. Nothing in it is a static mock-up.
| Screen | What it does |
|---|---|
Inventory Dashboard (/dashboard) | Compliance score that updates as CIPC, BEE and tax documents are uploaded or removed. Colour-coded stock tracker. A low-stock alert slides down when an item drops into Red. Quick Fix adds the missing quantity to the cart and opens checkout. |
Ordering & Checkout (/order) | A catalogue of verified products only, each with SABS / Verified Supplier badges. The cart shows bulk discounts as a separate line. Payment toggles are PayShap, Instant EFT and Cash on Delivery. A live delivery timeline follows each order. |
Financial Portal (/finance) | Monthly spend chart and bank scorecard (total spent, on-time payments, credit health), plus a one-tap Financial Health Report PDF with the SpazaLink logo. |
npm install
cp .env.example .env.local # fill in MONGODB_URI and REPORT_SIGNING_SECRET
npx clerk@latest env pull # writes the Clerk keys for the linked "Spaza Link" app
npm run seed # wholesalers, catalogue, two demo shops + their Clerk users, sample PDFs
npm run dev # http://localhost:3000
Before you submit, run the security checks against a production build:
npm run build && npm start # terminal 1
npm run test:brief # terminal 2: 80 checks, one per item in the project brief (writes BRIEF_TEST_REPORT.md)
npm run test:security # terminal 2: 66 security, role, demo shop and data wall checks
Windows clock. Clerk rejects session tokens that look "issued in the future". If your PC clock is more than a few seconds off, sign-in loops back to the sign-in page. Sync it under Settings → Time & language → Date & time → Sync now.
| Password | ||
|---|---|---|
| Demo shop (Thandi's Spaza, Soweto) | [email protected] | Spaza@2026 |
| Second shop, for showing the data walls (Sipho's Corner Store) | [email protected] | Spaza@2026 |
| Platform admin (admin portal) | [email protected] | Spaza@2026 |
If Clerk asks for a verification code, enter 424242. That code works for any +clerk_test address on a development instance. The same accounts are shown on the sign-in page with copy buttons.
Thandi's shop is preloaded with stock in all three colours, six months of orders, and CIPC + BEE documents (85% Ready). Sipho's shop has no documents, so you can demo uploads live with samples/sample_cipc_certificate.pdf and samples/sample_bee_affidavit.pdf. Run npm run seed again at any time to reset both demo shops. It also resets the demo users' passwords in Clerk.
Thandi's Spaza is the shared demo shop (isDemo: true in the seed).
DEMO_READ_ONLY./admin)Open to the Platform admin role only. The role is stored in the Clerk user's publicMetadata.role and checked on the server for every admin request. Everyone else gets an "Admins only" screen, and the API answers 403.
| Page | What it shows | Forms and actions |
|---|---|---|
| Overview | Key figures, registrations per month, newest shops, order value, orders, deliveries by status, payment mix, best sellers, sales by category, stock health per shop, credit health, compliance, critical stock, recent orders | Platform report download (PDF or PowerPoint) |
| Shops | Every shop with joined date, compliance, a stock-health bar, orders, spend, on-time rate and credit health. Includes credit health and compliance charts | Select a shop for its monthly spend chart |
| Orders | Every order with payment outcome. Includes a status donut and payment outcome chart | Status tabs, search, Move on (advances delivery) |
| Products | The catalogue, with a status donut and products per category | Add product, with a live price check chart against its category. Edit price, bulk deal and verification |
| Suppliers | Every wholesaler, with a status donut, wholesalers per province and order value per wholesaler | Add supplier, with a live province chart. Change verification inline |
| Guide | A plain-language explanation of how SpazaLink works |
All charts are shadcn/ui charts (src/components/charts.tsx).
When anyone signs in, a one-time notice explains that access is role based today, and that full RBAC is coming: named permissions per role, with an audit trail.
Each report offers a choice of PDF document or PowerPoint slides (native, editable charts):
/finance): GET /api/report?format=pdf|pptx/admin): GET /api/admin/report?format=pdf|pptx/finance, this month's point on the chart now includes the new order.Payments run in sandbox mode: no money moves. State this on the slides.
Palette (taken from the logo): navy #062654 for trust and primary actions, pink #F2366D for energy and highlights, and mustard #F4AF23 for attention, savings and Quick Fix, all on a soft blush background. Stock status keeps its own green, amber and red.
Components: shadcn/ui (Radix) in src/components/ui, wrapped by the app's kit in src/components/kit.tsx: Button, Card, Input, Badge, Select, AlertDialog for destructive confirmations, and Sonner toasts. Clerk's screens use @clerk/ui's shadcn theme in the same colours.
Logo: the master artwork is brand/spaza-link.png (a copy is in public/brand/). npm run brand cuts out the white background and generates:
favicon.ico, icon.png, apple-icon.pngopengraph-image.png)The logo appears in the site header, the app header, the sign-in panel, the About section, the footer and the PDF report.
Copy: the interface avoids dashes by design.
src/lib/rules.ts).Shop owner (phone / desktop, installable PWA)
│ HTTPS
▼
Next.js 16 app (App Router, Tailwind CSS 4, shadcn/ui)
├─ Clerk: sign-in / sign-up pages, UserButton, session cookies
├─ proxy.ts: clerkMiddleware(), app pages require a session
├─ Inventory Dashboard ─┐
├─ Ordering & Checkout ─┼─ client components → /api/* route handlers
├─ Financial Portal ────┘
└─ API routes: Clerk auth() → owner's shop → shop-walled data layer → MongoDB
▼
MongoDB Atlas (collections + GridFS "documents" bucket)
| Layer | Choice |
|---|---|
| Frontend | Next.js 16 (App Router), Tailwind CSS 4, shadcn/ui (Radix), lucide icons, Recharts |
| Backend | Next.js API route handlers (Node.js runtime) |
| Database | MongoDB Atlas, with application-level row security (src/lib/walled.ts) |
| Auth | Clerk (@clerk/nextjs): email/password, Google, email codes, device verification, bot and brute-force protection |
| File storage | MongoDB GridFS (documents bucket) |
| PDF reports | pdf-lib, generated server-side |
| Hosting | Vercel (set the env vars below) |
| Build file | Installable PWA (public/manifest.webmanifest); can be wrapped as a signed APK with Bubblewrap / PWABuilder |
shops.ownerUserId is unique.withShop() (src/lib/api.ts). It calls Clerk's auth(), looks up that user's shop, and hands the handler a walled(db, shopId) accessor.walled() merges shopId into every filter, insert, update, delete and aggregation. It's written last, so request data can never override it.NO_SHOP, and the app sends them to /onboarding.| Collection | Holds | Key fields | Walled by shop |
|---|---|---|---|
shops | One per spaza shop | ownerUserId (Clerk user id, unique), name, location, phone | Yes (owner = session user) |
compliance_documents | CIPC / BEE / TAX uploads | shopId, type, fileId → GridFS, filename, mime, size, uploadedAt | Yes |
suppliers | Wholesalers | name, region, verificationStatus, badges | No (read-only) |
products | Wholesaler catalogue | supplierId, name, unit, price, bulkRule, badges, verificationStatus | No (read-only) |
stock_items | The shop's own stock | shopId, productId, quantity, fullCapacity | Yes |
orders | Each order placed | shopId, orderNumber, subtotal, discount, total, paymentMethod, status | Yes |
order_items | Lines in an order | shopId, orderId, productId, quantity, unitPrice, discount, lineTotal | Yes |
order_status_events | Delivery timeline steps | shopId, orderId, status, changedAt | Yes |
payments | Payment records | shopId, orderId, method, amount, status, paidAt, paidOnTime, sandbox | Yes |
documents.files / .chunks | GridFS file storage | metadata.shopId, metadata.type | Yes (served only via a walled lookup) |
User accounts, passwords and sessions live in Clerk, not in MongoDB.
| Stage | What we do | Evidence / notes |
|---|---|---|
| Requirements | Protect shop documents, orders, payments, financial reports and identities | Walled collections are listed in src/lib/walled.ts; identities are delegated to Clerk |
| Design | Threat-model the three screens; one data-access path per shop | Clerk session → owner's shop → walled(); read-only catalogue; server-side pricing |
| Implementation | Input cleaning, upload filters, secrets out of code, HTTPS only | src/lib/validation.ts (zod + text cleaning), src/lib/uploads.ts (extension + MIME + magic bytes + 5 MB), .env.local ignored by git, HSTS and security headers in next.config.ts |
| Testing | Cross-account access, roles, bad input and upload tests; every brief item | npm run test:security (55 checks) and npm run test:brief (77 checks), all passing against real Clerk sessions and MongoDB |
| Deployment | Environment variables for keys; least-privilege database user | Env vars in Vercel; Atlas user with readWrite on spazalink only; Clerk production instance via npx clerk@latest deploy |
| Maintenance | Dependency updates, error logging, access review | npm audit; server errors logged as [api] without leaking details; Clerk dashboard for sessions and users |
walled.ts + test suite)auth() in each route).exe, a renamed .exe and a 6 MB PDF are each refused with a clear messagenosniff / X-Frame-Options: DENY / HSTS, and Cache-Control: no-store on documents and reports| Name | Purpose |
|---|---|
MONGODB_URI, MONGODB_DB | Atlas connection |
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, CLERK_SECRET_KEY | Clerk (pulled with npx clerk@latest env pull) |
REPORT_SIGNING_SECRET | Signs the integrity code printed on Financial Health Reports |
npx clerk@latest deploy and swap in production keys. The current keys are for a development instance.Vercel limits request bodies to 4.5 MB, so on Vercel the practical upload limit is about 4.4 MB. The server still enforces the 5 MB rule.
brand/spaza-link.png master logo artwork
src/
proxy.ts clerkMiddleware page protection
components/ui/ shadcn/ui components
components/kit.tsx SpazaLink UI kit on top of shadcn
components/logo.tsx Logo (lockup / mark / full) + BrandStripe
lib/
rules.ts business rules shared by client and server
walled.ts application-level row security
api.ts withShop() / withUser() on Clerk auth(), errors, origin check
clerk-appearance.ts Clerk theme in SpazaLink colours
validation.ts, uploads.ts, services.ts, db.ts, ...
app/
page.tsx landing + About
(auth)/sign-in, sign-up, onboarding
(app)/dashboard, order, finance
api/... route handlers
scripts/
seed.mjs demo data + Clerk demo users
security-check.mjs SSDL test evidence
brand-assets.mjs logo, favicon and share image generation