Commission independent, ground-level business and welfare verification from anywhere in the world — and get evidence-backed reports back within days.
VeriBridge is a full-stack oversight platform built for diaspora clients who need eyes on the ground in Africa. Whether you're a UK-based investor funding a Lagos construction project, a family member arranging welfare visits for an elderly relative, or a business owner who needs supplier legitimacy confirmed before wiring money — VeriBridge connects you with a vetted local field team, handles scheduling and reporting, and gives you a real-time oversight dashboard backed by AI-assisted analysis.
The platform covers two verticals: Business Oversight (verification visits, operational monitoring, investor-confidence reporting) and ElderCare Oversight (wellbeing visits, welfare monitoring, appointment accompaniment). Both flow through the same backend: subscription plans with feature permissions, one-off bookable orders, Stripe payments with webhook activation, and a staff assignment + reporting layer.
This is a curated portfolio excerpt, not the full application. VeriBridge is an active commercial product, so the complete codebase — all Blade views and CSS, admin tooling, full test suite, deployment config, credentials, and internal business logic — stays private.
What's published here is a representative cross-section of the backend: the authentication architecture, payment orchestration, middleware chain, key models and services, route definitions, and the database schema history. Files in this repo reference controllers, models, and helpers (the temp() view helper, main() settings helper, the full bootstrap/app.php wiring) that exist in the full application but aren't included here.
This repo isn't runnable standalone. It's here to show how the system is structured, the decisions behind it, and what the code actually looks like.
| Homepage | Packages |
|---|---|
![]() |
![]() |
| Pricing | Dashboard |
|---|---|
![]() |
![]() |
| Bridge AI | Contact |
|---|---|
![]() |
![]() |
Investing money or placing trust in a business or caregiver you can't physically visit is a high-stakes leap of faith. Bank transfers are sent to contractors who may not exist. Elderly relatives are left with carers whose visits are never actually confirmed. Supplier relationships are built on WhatsApp profiles and nothing more.
VeriBridge closes that gap with human verification: a local field agent visits the location, documents what they find with photos and video, files a structured report, and delivers it to the client's dashboard — with an AI analysis layer (Bridge AI) that cross-references findings against stated claims and flags anomalies.
For clients (diaspora users)
- Project-scoped dashboard: every active oversight project has its own data view, health score, and report history
- Plan-gated feature access: site visit history, live oversight view, analytics dashboard, Bridge AI insights — each unlocked at a different subscription tier
- One-off bookable visits: pay per visit, no subscription required, still get full portal access
- KYC verification gate: optional — admin can toggle whether KYC must be approved before accessing project features
- Stripe-powered subscription checkout and one-time order payments, with Stripe webhook activation and a reliable success-redirect fallback
- PDF reports, evidence attachments, and real-time order status tracking
- Two-factor authentication, single-session enforcement, and email verification
For field staff
- Assigned order queue with client and service detail
- Report submission with structured findings, recommendations, and evidence uploads
- Client notification triggered automatically when a report is submitted or approved
For admin (control panel)
- Full user, subscription, and order management
- Manual plan assignment, subscription extension, cancellation, and reactivation
- Subscription plan builder with JSON permission keys that gate individual portal features
- Email template management: slug-based, shortcode-driven, DB-overridable with hardcoded fallbacks
| Layer | Technology |
|---|---|
| Backend | PHP 8.2, Laravel 11 |
| Authentication | Multi-guard (user / staff / control), custom middleware, Laravel Sanctum-style session guards |
| Database | MySQL, Eloquent ORM |
| Payments | Stripe Checkout (subscriptions + one-time), webhooks with signature verification |
Laravel Mail, DynamicMail mailable, slug-based EmailTemplate model with DB + hardcoded fallbacks |
|
| Push notifications | Firebase Cloud Messaging via PushNotificationService |
| Two-factor auth | TOTP + recovery codes via TwoFactorAuthService, challenge middleware |
| Frontend | Blade templating, custom CSS design system (no Tailwind), vanilla JavaScript |
| Languages | PHP 8.2, Blade, JavaScript |
| File storage | Laravel Storage (local/S3 compatible) |
| Queue | Laravel Queues (database driver) |
Laravel 11 ships without Http/Kernel.php. All guards, middleware aliases, and route group wiring live in a single bootstrap/app.php. Three completely isolated portals share the same codebase without touching each other's session or middleware:
Client browser Staff browser Admin browser
│ │ │
/user/* /staff/* /control/*
│ │ │
guard:user guard:staff guard:control
check.email middleware:staff middleware:control
single.session:user middleware:active middleware:active
check.kyc
check.plan:feature
│ │ │
└──────────────────────┴──────────────────────┘
│
bootstrap/app.php
(one place — all wiring)
│
┌─────────┴─────────┐
│ Controllers │
│ User\ Staff\ │
│ Control\ │
└─────────┬─────────┘
Each guard has its own controller namespace, route file, URL prefix, and named-route prefix (user.*, staff.*, control.*). No guard can accidentally resolve another guard's authenticated user because every middleware explicitly names its guard: auth('user')->user(), Auth::guard('staff')->check(), etc.
Every Stripe payment — whether a recurring subscription or a one-time booking — follows the same two-path activation design to handle the inherent race between the Stripe-hosted checkout page and the webhook:
Client selects plan / order
│
Controller creates DB record (status: PENDING / payment_status: UNPAID)
│
Stripe Checkout session created, stripe_session_id saved to record
│
Client redirected to Stripe ──────────────────────────────────────┐
│ │
│ (payment succeeds) │
│ │
Path A: Stripe redirect to success_url Path B: Stripe webhook
(immediate, always fires) (may be delayed or blocked
│ on localhost)
│ │
success() / paymentSuccess() handleCheckoutCompleted()
– find record by stripe_session_id – find record by stripe_session_id
– fallback: Stripe API retrieve + metadata – atomic update (WHERE status=PENDING)
– atomic status update (WHERE PENDING) – create Payment + Invoice records
– create Payment + Invoice records – activate project
(idempotency: check transaction_ref first) – send activation email
– activate project
– send activation email
│ │
└──────────────────────────┬────────────────────────────────┘
│
Both paths safe to run in any order.
Idempotency guards prevent double records.
See StripeWebhookController.php and SubscriptionController.php.
Each project has a project_data table of key–value data points (ProjectData records) with a status field: excellent, good, average, issue, confusing, critical. The dashboard aggregates these into a 0–100 health score (weighted average of per-status scores), which drives the donut chart and per-metric progress bars. No data → score is 0, not a fake default. See MainController.php.
Feature gating is data-driven, not code-driven. Every subscription plan stores a JSON features column in the shape {"permissions": ["site-visits", "analytics", "bridge-ai"], "items": ["display label 1", ...]}. The check.plan:bridge-ai middleware reads the permissions array at request time — adding a new gated feature to a plan is a database update, not a deployment. See CheckPlanPermission.php.
The success-redirect is the primary activation path, not the webhook. On localhost (during development) Stripe webhooks never arrive. Rather than require Stripe CLI forwarding for basic testing, the success() and paymentSuccess() handlers are fully self-contained: they try the stripe_session_id DB lookup, fall back to a Stripe API session retrieve using the URL parameter, use session metadata to locate the correct DB record, and create Payment + Invoice records with a transaction_ref idempotency guard. The webhook runs the same logic with an affected === 0 early-return so whichever path wins, the other is a no-op.
One main() helper, one settings row, cached. All site-wide settings (contact email, phone, social links, feature toggles like kyc_verification and two_factor_auth) live in a single settings table row fetched once per process via Cache::remember('site_settings', 300, ...). The main($key) global helper reads from that cache. Changing a setting takes effect within five minutes without touching code or config files.
Email templates are DB-first with hardcoded fallbacks. EmailTemplate::getBySlug() checks the database first (so non-technical admins can edit copy from the control panel), then falls back to a $defaults array in the model for slugs that haven't been customised yet. This means new slugs work immediately on deploy without a DB record, and the fallback is never null. See EmailTemplate.php and EmailTemplateService.php.
Report emails fire from a model observer, not a controller. VerificationReport::booted() registers an updated observer that watches for status changing to SUBMITTED or APPROVED and emails the client. This means the notification fires regardless of which controller or admin tool changes the status — one place, no duplication. See VerificationReport.php.
KYC is an opt-in gate, not always-on. The check.kyc middleware reads main('kyc_verification') — a boolean toggle in the settings row. When KYC enforcement is off (default), the middleware is a no-op. When on, unapproved users are redirected to the KYC flow but can still access their profile, KYC submission, and dashboard. This prevents a misconfigured toggle from locking users out of their accounts entirely. See CheckKYC.php.
Single-session enforcement is guard-aware. SingleSession middleware stores a session token in the DB (users.session_id) and compares it against the current session on every authenticated request. If another device logs in, the old session is invalidated immediately on the next request — no polling, no cron, no WebSockets. The guard name is passed as a parameter so the same middleware works for user, staff, and control guards independently.
Slug-based routing with optional {uuid?}. The entire user portal is prefixed with project/{uuid?}. If a UUID is present, the Guard middleware switches the authenticated user's active project to the one referenced. If absent, it resolves to the current active project. This means a user can deep-link directly to a specific project's dashboard or orders from an email, and the URL is also shareable between browser tabs.
The full application has 32 database tables across three verticals (Business Oversight, ElderCare Oversight, and platform administration). The database/migrations folder contains the major structural migrations; additive/incremental migrations are omitted from this excerpt. Key tables:
| Table | Purpose |
|---|---|
users |
Client accounts — active_project_id FK, preferences JSON, session_id for single-session enforcement |
projects |
Client oversight projects — uuid, slug, status (DRAFT → ACTIVE), service_id |
project_data |
Key–value data points per project — variable_key, value_text, value_json, data_type, status, sort_order |
services |
Service catalogue — service_type (ONE_OFF / SUBSCRIPTION), base_price |
service_categories |
Category groupings — slug used for plan filtering (e.g. eldercare-oversight) |
subscription_plans |
Plan definitions — features JSON with permissions + items arrays, price_gbp, billing_cycle |
subscriptions |
Client subscriptions — status enum (PENDING / ACTIVE / SUSPENDED / CANCELLED), stripe_session_id, stripe_subscription_id |
verification_orders |
One-off bookings — payment_status (UNPAID / PAID), status (PENDING → IN_PROGRESS → COMPLETED), stripe_session_id |
verification_reports |
Field team reports — findings, recommendations, status (DRAFT → SUBMITTED → APPROVED), evidence attachments |
payments |
Append-only payment ledger — transaction_ref for idempotency, gateway_reference |
invoices |
Invoice records — linked to subscription or order |
kyc_submissions |
KYC document submissions — status (PENDING / APPROVED / REJECTED) |
email_templates |
Overridable email copy — slug, subject, body with {{shortcode}} placeholders |
settings |
Single-row site configuration — all toggles and contact details |
staff |
Staff member accounts (separate from users) |
controls |
Admin accounts (separate guard from both users and staff) |
├── artisan # Laravel CLI entry point
├── composer.json # dependencies (laravel/framework 11, stripe/stripe-php, …)
├── .env.example # environment variable template
│
├── bootstrap/
│ └── app.php # single-file wiring: guards, routes, middleware aliases (Laravel 11)
│
├── config/
│ └── auth.php # three-guard definition: user / staff / control
│
├── public/
│ └── index.php # HTTP entry point — boots the application
│
├── resources/
│ └── views/
│ ├── layout/
│ │ └── app.blade.php # public site layout (home, pricing, contact)
│ └── main/
│ ├── layout/
│ │ └── master.blade.php # authenticated portal layout (sidebar, header, notifications)
│ └── user/
│ └── dashboard.blade.php # client dashboard — health score, KPI row, activity feed
│
├── app/
│ ├── Http/
│ │ ├── Controllers/
│ │ │ ├── Ipn/
│ │ │ │ └── StripeWebhookController.php # checkout.completed, invoice.paid, subscription events
│ │ │ ├── User/
│ │ │ │ ├── SubscriptionController.php # plan checkout, success redirect, cancel
│ │ │ │ ├── VerificationOrderController.php # one-off order booking + payment
│ │ │ │ ├── ProjectController.php # project CRUD, active project switching
│ │ │ │ └── MainController.php # dashboard, oversight health score
│ │ │ ├── Staff/
│ │ │ │ └── StaffController.php # assigned orders, report submission
│ │ │ └── Control/
│ │ │ ├── ManagementController.php # admin list views (users, orders, invoices)
│ │ │ └── SubscriptionManagementController.php # plan assign, extend, cancel, reactivate
│ │ └── Middleware/
│ │ ├── Guard.php # multi-guard auth + active project resolution
│ │ ├── CheckPlanPermission.php # feature gate: plan.features.permissions
│ │ ├── CheckKYC.php # optional KYC gate, toggled via settings
│ │ ├── RequireTwoFactor.php # TOTP challenge enforcement
│ │ ├── SingleSession.php # one-device-at-a-time enforcement
│ │ └── CheckEmailVerification.php # blocks unverified accounts
│ ├── Models/
│ │ ├── User.php # portal access, planHasFeature(), relationships
│ │ ├── ClientSubscription.php # subscription lifecycle + Stripe fields
│ │ ├── VerificationOrder.php # order lifecycle, payment relationship
│ │ ├── VerificationReport.php # booted() observer → email on status change
│ │ ├── Project.php # HasMany customStats, reports, orders
│ │ ├── SubscriptionPlan.php # features JSON, billing cycle
│ │ ├── Payment.php # transaction_ref idempotency key
│ │ ├── Invoice.php # billing invoice records
│ │ └── EmailTemplate.php # getBySlug(), $defaults fallback
│ └── Services/
│ ├── EmailTemplateService.php # send() via slug + shortcode substitution
│ ├── TwoFactorAuthService.php # TOTP generation, verification, recovery codes
│ └── PushNotificationService.php # FCM push notifications
│
├── database/
│ ├── migrations/ # 9 core structural migrations (32 tables total)
│ └── seeders/
│ ├── VeriBridgeServicesSeeder.php # business oversight plans with permission keys
│ └── ElderCareSeeder.php # eldercare plans (Silver / Gold / Platinum)
│
├── routes/
│ ├── user.php # client portal route tree (plan-gated)
│ ├── staff.php # staff portal routes
│ └── control.php # admin panel routes
│
└── screenshots/ # portfolio screenshots
This repo isn't runnable standalone — views, config, the bootstrap/app.php guard wiring, helpers, and several referenced models aren't included. The best way to read it:
-
Start with
routes/user.phpto understand the full portal surface: what's public, what requires authentication, what's gated by plan permission, and howproject/{uuid?}scoping works. -
Read
Guard.phpnext. It resolves which project is active, switches it when the UUID in the URL changes, and redirects new users to project creation. Everything in the authenticated portal flows through it. -
Follow a subscription payment:
SubscriptionController::checkout()→ Stripe hosted page →SubscriptionController::success()(success redirect path) andStripeWebhookController::handleSubscriptionCheckoutCompleted()(webhook path). Both activate the same DB record, both create Payment and Invoice records, both send an email — with idempotency guards so only one actually writes. -
Look at
CheckPlanPermission.phpalongsideVeriBridgeServicesSeeder.phpto see howfeatures.permissionson a plan maps to middleware keys on routes. -
Open
VerificationReport.phpfor the model observer pattern:booted()registers anupdatedhook that emails the client when a report status changes — no controller coupling. -
Read
EmailTemplate.phpandEmailTemplateService.phptogether to see the DB-first / hardcoded-fallback email system.
Business Oversight (monthly-business-plans / one-time-verification)
| Plan | Price | Permission keys unlocked |
|---|---|---|
| Business Verify | £79/mo | site-visits |
| Business Monitoring | £199/mo | site-visits, analytics, oversight |
| Business Oversight Premium | £499/mo | site-visits, analytics, oversight, bridge-ai, multi-project |
| Investor Confidence | £750+/mo | + priority-support |
| Business Verification Visit | £149 one-off | portal access (no plan features) |
| Supplier/Partner Verification | £199 one-off | portal access (no plan features) |
ElderCare Oversight (eldercare-oversight)
| Plan | Price | Permission keys unlocked |
|---|---|---|
| Standard Wellbeing Visit | £75 one-off | portal access |
| Silver Package | £199/mo | wellbeing-visits, family-reports, welfare-monitoring |
| Gold Package | £399/mo | + appointment-support, priority-comms |
| Platinum Package | Custom | + dedicated-liaison (no Stripe checkout — contact form) |
The code in this repository is shared for portfolio and reference purposes under the MIT License. This is a curated excerpt of a larger closed-source production application; the licence covers what's shown here, not the complete VeriBridge platform.
Built by THE SOG — GitHub





