Example audit — performed on the public demo repository dovito-public/dovito-ad-display-demo at commit d4a9016. Reproduces the same methodology we apply to paid client engagements. All findings verifiable in the linked repo · live demo.

Codebase Audit Report

April 9, 2026 · Refresh PUBLIC CASE STUDY
Public Demo Audit · Refreshed

Dovito Ad Display Demo Audit

Frontend-only static demo – architecture, code quality, security, performance, and SaaS-readiness assessment

A worked example of our standard audit methodology, applied to a public, backend-amputated demo of a digital signage platform. Re-audited after a round of targeted fixes to the mock layer, demo UX, and asset pipeline. Every finding in this report can be reproduced by cloning the repo at the audited commit.

13,595 Lines of Code
75 TS/TSX Files
17 Security Findings
C+ Overall Grade

Executive Summary

Context, purpose, and headline findings for the refreshed public demo audit.

The subject of this audit is dovito-ad-display-demo — a public, frontend-only fork of the Dovito Ad Display production codebase. Every server-side component (Postgres, NextAuth, Stripe, Supabase, Mailgun) has been surgically removed and replaced with an in-browser mock layer that intercepts window.fetch and persists state to localStorage. It is intentionally not a production system, and this audit evaluates it on those terms.

This is a refresh of an audit originally performed earlier on the same day at commit f6d21ca. Between then and commit d4a9016, ten targeted commits addressed a cluster of correctness, UX, and asset-pipeline issues surfaced by the first pass — most notably a React error #185 infinite render loop, a demo banner that was covering the fixed top nav, an internal navigation link that pointed at a hardcoded Stripe billing URL, a login page that was overbuilt for a demo, a track-status page that relied on a free-form email input, eight placeholder ad JPEGs (now hand-crafted SVGs), a missing basePath on plain <img> tags, and silent failures in useQuery calls that were missing an explicit queryFn. The overall grade moves from C to C+ as a result of these fixes, and the security-finding count drops from 19 to 17.

The codebase's three most notable strengths at d4a9016: (1) a clean, well-factored mock-layer triadmock-fetch.ts patches window.fetch exactly once, mock-api.ts routes /api/* through a single pure function with ~50 routes, and all mutations flow through a tiny CRUD helper in mock-store.ts; (2) a resilient React Query configuration — a default queryFn routes any queryKey-based call through mockGet(), which means components can use useQuery({ queryKey: ["/api/..."] }) without boilerplate and without the silent-failure bug that existed at f6d21ca; and (3) a defensive cached snapshot in mock-auth.ts that ends the React #185 loop by returning a stable User reference until a notify() call flips the dirty bit.

The three most significant remaining concerns — each intentional on the demo, critical in production — are unchanged from the first pass: (1) no auth enforcement; any visitor reaching /admin sees the admin UI (though the new login page provides a single explicit "Sign in as admin" button that documents the demo contract honestly); (2) mock credentials and seeded data are fully readable client-side; and (3) source maps are still being shipped in the GitHub Pages build — one .js.map file is present under out/_next/static/chunks/, and next.config.ts still lacks a productionBrowserSourceMaps: false toggle. On the SaaS-readiness dimension, the demo still scores ~10% — a structural consequence of having no backend.

The replacement value of the frontend work lands between $60K and $180K depending on team structure. Productising the demo into a real multi-tenant SaaS would require a further $150K–$300K of backend, auth, billing, and platform work.

What changed since the first pass. The following findings from the earlier audit have been resolved at d4a9016: React #185 infinite render loop in useAuth; demo banner obscuring the top nav; a hardcoded Stripe URL in the navigation login button; placeholder ad JPEGs; basePath missing on plain <img> tags; useQuery calls silently failing when no queryFn was supplied; default favicon; and two ad SVGs with unescaped ampersands that produced malformed XML. Two tabnabbing findings from the first pass are also gone — one link was removed outright, and the other was internal after the login refactor.
Why publish this? Our paid audits produce a client-facing report that the client rarely shares. This case study is identical in structure, scope, and methodology — the only difference is that the subject is a repository anyone can clone. When we tell a prospect "we'll deliver a report like this," this is the document we point to.

System Overview

Technology stack, architecture, and codebase metrics at the audited commit.

13,595Lines of Code
75TS/TSX Files
19Page Routes
~50Mock API Endpoints
26Prod Dependencies
0Tests

Technology Stack

LayerTechnologyVersion
FrameworkNext.js (App Router, static export)16.1.7
UI RuntimeReact19.2.3
LanguageTypeScript (strict mode)5.x
StylingTailwind CSS + shadcn/ui4.x
Data fetchingTanStack React Query5.90
Forms / validationreact-hook-form + Zod7.71 / 4.3
Animationframer-motion12.34
Mock persistencebrowser localStorage (key dovito-demo-v2)
DeploymentGitHub Pages (static)

Architecture

Presentation Layer
Next.js 16 App Router React 19 Tailwind 4 shadcn/ui React Query (default queryFn) react-hook-form + Zod
Mock Service Layer (~1,100 LOC)
mock-fetch.ts (window.fetch patch) mock-api.ts (router, ~50 endpoints) mock-auth.ts (cached snapshot) mock-store.ts (v2 schema, CRUD)
Persistence Layer
localStorage key: dovito-demo-v2 mock-data.ts (seed + asset() helper)

What Was Removed

Relative to the production codebase at Dovito-Ad-Display-V2.0.0, the demo strips:

RemovedReplaced With
Next.js API routes (src/app/api/*)Pure-function router in mock-api.ts
PostgreSQL + Drizzle ORMlocalStorage-backed collections
NextAuth v5 (Google OAuth, credentials)Single admin button + email-prefix role inference
Stripe (Checkout, Elements, webhooks)Fake card inputs in collect-payment/page.tsx
Supabase Storage (image uploads)Hand-crafted SVG ad assets in /demo-images/
Nodemailer / MailgunNo-op success responses
Demo-specific deviations from source. Three files are materially different from production: src/app/(public)/collect-payment/page.tsx renders a fake card form; src/app/(public)/apply/page.tsx has Stripe calls removed; and src/app/login/page.tsx is stripped to a single "Sign in as Admin" button with a "Back to home" link. All other changes are confined to the mock layer, assets, the demo banner, and the default query client.

Code Quality Scorecard

Letter grades across nine dimensions. Several are structurally capped by the static-demo constraint; we flag them rather than inflate them.

ArchitectureA-
Code ConsistencyB+
Type SafetyB-
Error HandlingD
Test CoverageF
SecurityC
PerformanceC+
DocumentationB
Developer ExperienceB+
OverallC+

Key Findings

Strength

Resilient default queryFn

src/lib/query-client.ts:8 installs a default queryFn that routes queryKey[0] through mockGet(). Before this fix (commit d4a9016), any useQuery call lacking an explicit queryFn silently failed because the previous QueryClient had none. Now the whole app can rely on "queryKey: ["/api/..."]" as a calling convention.

Strength

Cached auth snapshot fixes React #185

src/lib/mock-auth.ts:13 introduces cachedSnapshot + snapshotDirty. getSnapshot() now returns the same User reference between notifications, so useSyncExternalStore stops looping. src/hooks/use-auth.ts:16 also wraps the adapted user in useMemo so consumers with [user] in effect deps don't re-run every render.

Strength

Clean mock-layer triad (~1,100 LOC)

mock-fetch.ts patches window.fetch once; mock-api.ts is a single pure function with ~50 routes; mock-store.ts exposes a handful of generic insertOne/updateOne/deleteOne/findOne helpers. The triad is small enough to audit end to end in an afternoon.

Strength

Strict TypeScript with justified escape hatches

tsconfig.json has "strict": true. The mock layer itself contains one eslint-disable for no-explicit-any on the root store type (mock-store.ts:19) — structurally justified because the store is a heterogeneous collection keyed by name.

Medium

~25 any usages in application source

The earlier audit claimed zero any outside the mock layer. That was an overreach: a refreshed grep finds any assertions in marketing-calculator.tsx:27, embedded-display.tsx:30, app/display/page.tsx:38, brand-kit/page.tsx:69, apply/page.tsx:927, and three success/collect-payment pages. Most are (x as any[]).find((s: any) => ...) patterns where a typed DisplaySetting[] would be trivial. Corrected downgrades Type Safety from A to B-.

High

No failure paths in mock API

mock-api.ts:469 falls back to { success: true, demo: true, path, method } for unrecognised routes. Downstream code never exercises its error branch, and any typo in a queryKey will succeed silently with a confusing payload. Would become a production bug the moment the mock is swapped for a real backend.

High

Zero test coverage

No test files, no runner, no CI gate. Carry-over from production. Even a Vitest smoke test against mock-api.ts's routing table would prevent a whole class of regressions on a demo whose sole purpose is a convincing walkthrough.

Medium

localStorage reads on every store access

mock-store.ts:41's load() is called inside every public helper. For ~200 seed records this is microseconds, but it blocks the main thread and scales poorly. An in-memory mirror with write-through persistence would cost <30 LOC.

Medium

No React error boundaries

No ErrorBoundary component in the tree. An uncaught render error below the root layout blanks the entire page. Single root-level boundary would cost <20 LOC and catch the kind of silent XML-parse failures that shipped ad-1/ad-2 with unescaped ampersands earlier today.

Info

localStorage schema versioning

mock-store.ts:17 bumps the key from dovito-demo-v1 to dovito-demo-v2 whenever the seed shape changes — here it was bumped after basePath-prefixed image URLs replaced raw strings. The mechanism is good; there is no automatic detection ("did the seed change?") so bumping remains a manual discipline.

Info

Eight custom SVG ad assets replace placeholder JPEGs

All eight slide ads under public/demo-images/ are now hand-crafted SVGs, one per business. All eight parse cleanly as XML (verified). Earlier today two of them (ad-1, ad-2) shipped with unescaped ampersands — the fix is in, but the silent failure mode is worth a note below in Asset Validation.

Info

Custom SVG favicon

public/icon.svg is a bespoke SVG favicon; the default favicon.ico has been removed. Non-functional but removes a "generic Next.js starter" tell from the client.

Asset Validation

Earlier today two ad SVGs shipped to the live demo with unescaped & characters in their <text> nodes, producing malformed XML that browsers refused to render. The fix was a straightforward escape, but the lesson is that static SVGs are entering the bundle with zero validation. A single xmllint --noout public/demo-images/*.svg gate in the build would have caught it. We flag it as Info because it is already fixed, but the systemic gap remains.


Security Findings

17 findings across 5 severity levels at commit d4a9016. Several are flagged as "intentional on demo, critical in production" — the point of listing them is to make explicit what a real deployment would need to address.

0Critical
2High
5Medium
4Low
6Info

High Severity Findings

High – SEC-001

Source maps shipped to production

next.config.ts does not set productionBrowserSourceMaps: false, and one .js.map file (a6dad97d9634a72d.js.map) is present under out/_next/static/chunks/ in the deployed build. On a static GitHub Pages deployment this means at least part of the original TypeScript source is trivially recoverable. On the demo this leaks nothing sensitive; on any real engagement it would leak client-side code paths to adversaries. Unchanged from earlier audit.

High – SEC-002

No HTTP security headers on GitHub Pages

GitHub Pages does not set Content-Security-Policy, X-Frame-Options, Strict-Transport-Security, Referrer-Policy, or Permissions-Policy. A static-export Next.js app cannot inject headers at runtime. For the demo this is acceptable; for any real deployment the app must be fronted by a CDN or edge worker capable of header injection. Unchanged.

Medium Severity Findings

IDFindingFileCategory
SEC-003No client-side auth enforcement: /admin is reachable by any visitor with full admin UI accesssrc/app/admin/page.tsxAuthZ (intentional)
SEC-004Role is inferred from email string containing "admin" or "super" — no password, no verificationsrc/lib/mock-auth.ts:21AuthN (intentional)
SEC-005Mock router fallback returns HTTP 200 + {success: true} for every unknown path, masking typos and future bugssrc/lib/mock-api.ts:469Error handling
SEC-0061 high + 3 moderate npm advisories in the full dependency tree (@hono/node-server, brace-expansion, transitive dev deps)package-lock.jsonDependencies
SEC-007localStorage state is unencrypted, unsigned, and fully attacker-controllable. On a real app this would enable client-side privilege escalation; the schema-version bump to v2 only protects against stale data, not tampering.src/lib/mock-store.tsData integrity (intentional)

Low & Info Findings

IDSeverityFinding
SEC-008LowDemo auth has no password field at all — the login page is a single "Sign in as Admin" button that is honest about demo mode but provides no identity gate.
SEC-009LowOne target="_blank" with explicit rel="noopener noreferrer" present on src/app/audit/page.tsx:59 — clean. The first audit's two tabnabbing findings are resolved: the navigation login button is now internal, and the audit page link was already properly annotated.
SEC-010LowCoupon code LAUNCH20 is hardcoded in mock-api.ts:453. Intentional for demo but would be a finding in production.
SEC-011LowMock admin credentials and seeded users are visible in the client bundle. Intentional and documented.
SEC-012InfoNo process.env secrets referenced in application source beyond NODE_ENV, which is used only for basePath resolution in mock-api.ts:12 and the audit page. Clean.
SEC-013InfoNo .env file committed to the repository.
SEC-014InfoNo dangerouslySetInnerHTML anywhere in the codebase.
SEC-015InfoNo inline event handlers (onclick=) in rendered JSX — all handlers are typed React props.
SEC-016InfoGitHub Pages serves over HTTPS with automatic certificate provisioning.
SEC-017InfoNo third-party analytics, tag managers, or trackers loaded in the client bundle.
Honest disclosure: The production Dovito Ad Display codebase — from which this demo is forked — has a real auth layer, CSRF protection, magic-byte image validation, Stripe webhook signing, rate limiting, and server-side authorization. None of those are relevant to a frontend-only demo, so none appear in this audit. The findings above are strictly the ones that apply to the artifact as shipped on GitHub Pages at d4a9016.

Performance Assessment

Directional assessment across five frontend categories. Lighthouse was not executed; numbers are based on static-file inspection of the deployed out/ directory at d4a9016.

Bundle Size
58%
Rendering
78%
Assets & Images
70%
Network
80%
Runtime
62%
2.0 MBJS Chunks (uncompressed)
8.3 MBTotal out/ directory
1Source map shipped
StaticHosting model

Performance Findings

Improved

Hand-crafted SVG ads shrink the image payload

The eight ad images are now vector SVGs averaging a few kilobytes each rather than ~100KB raster JPEGs. Total out/ drops from 9.8MB at the first audit to 8.3MB now, driven primarily by the demo-images/ swap. SVGs also render crisply at any display resolution, which matters for a digital-signage demo.

Frontend

Image optimization disabled

next.config.ts:8 sets images: { unoptimized: true } because static export cannot use the Next image optimizer. Structural constraint of GitHub Pages, not a bug — but worth flagging because the asset-pipeline finding above would be fully solved by reintroducing the optimizer on a real deployment.

Frontend

2.0MB JS chunks across ~40 files

out/_next/static/chunks/ is 2.0MB uncompressed — down from 2.2MB at the first audit. After gzip this is probably in the 550–750KB range, which is high but not extreme for a React 19 + framer-motion + radix-ui + React Query stack. No dynamic imports beyond Next.js route splitting.

Runtime

Synchronous localStorage on every mock call

mock-store.ts:72's getCollection calls load(), which does a full JSON.parse of the root store on every invocation. Even small admin pages trigger it 10+ times per render. For ~200 seed records this is microseconds, but it blocks the main thread and is trivially fixable.

Runtime

No memoization of expensive computations

Spot-checks of admin/page.tsx, marketing-calculator.tsx, and the analytics view show inline .filter().sort() chains in render bodies without useMemo. Not a bug, a finding worth flagging for growth.

Rendering

React Query stale-while-revalidate + 150ms mock delay

The new default queryFn in query-client.ts:8 combined with DELAY_MS = 150 in mock-api.ts:27 produces a convincing "real API" feel: skeletons appear briefly and data hydrates without blocking interactions.

Network

Trivial CDN caching

GitHub Pages serves every file with an ETag and modest cache headers. First paint after initial load is dominated by Next.js runtime and framer-motion, not network.

Info

Single artificial latency constant

Every mock call waits DELAY_MS = 150. Flat latency is unrealistic but serves the demo's purpose of showing skeletons and loading states.


SaaS Readiness

Condensed scorecard. The full 7-dimension deep-dive is published as a separate report.

10% Overall SaaS Readiness
7 Dimensions Assessed
Full rebuild Required for SaaS

Readiness by Dimension

Multi-Tenancy
0%
User Management
5%
Billing & Subscriptions
0%
Customization
30%
API Design
20%
Onboarding
15%
Monitoring
0%
Even when the answer to "can this become a SaaS?" is "not without a full backend rebuild," the structured gap analysis tells a prospect exactly what needs to be built and in what order. That is the deliverable, not the headline number.

Read the full SaaS Readiness Assessment →


Cost Analysis

Replacement value and team cost estimates for the 13.6K LOC frontend-only codebase. Methodology: industry-average productivity (roughly 10–15 LOC per engineer-hour including design, review, refactor), weighted by complexity.

$100KMid-case Cost
~1,000Development Hours
4–8 moCalendar Time

Engineering Cost by Rate

ScenarioHourly RateHoursTotal Cost
Low (solo contractor / offshore)$75/hr800–1,000$60K – $75K
Mid (US agency, mid-level)$125/hr800–1,200$100K – $150K
High (senior / boutique firm)$175/hr800–1,200$140K – $210K

Full Team Cost (Including Overhead)

Overhead multiplier accounts for PM, QA, design, code review, meetings, and context switching.

Company StageMultiplierMid-rate Cost
Solo / Founder1.0x$100K
Lean Startup1.45x$145K
Growth Company2.2x$220K
Enterprise2.65x$265K

Productisation Delta

What it would cost, on top of the figures above, to turn the demo into a real multi-tenant SaaS: backend + database + auth + real Stripe integration + email + image storage + tenancy + monitoring. Detailed breakdown in the SaaS Readiness report.

ScopeLowMidHigh
Backend + auth + DB + real payments + tenancy+$150K+$220K+$300K
Important framing: The demo's $60K–$180K replacement cost is not the cost of shipping the real Dovito Ad Display product. The production codebase is ~35% larger, includes a real backend, a real database, real Stripe integration, and real auth — its own cost analysis puts it materially higher.

Recommendations & Roadmap

A phased roadmap for anyone taking this demo forward — whether as a reference artifact, an internal tool, or the seed of a real product.

Phase 1 – Immediate (hours)

Finish cleaning up the shipped artifact

The second round of fixes knocked out eight issues already. These are the remaining avoidable findings, each a few-line change.

Set productionBrowserSourceMaps: false in next.config.ts Add xmllint gate to validate SVGs on build Replace ~25 any casts in application source Make the mock router's fallback return a 404-shape error in dev Run npm audit fix for the high + moderate advisories Add a root React error boundary
Phase 2 – Short-term (weeks)

Harden the demo as a reference artifact

If the demo keeps earning its keep as a sales artifact, these changes make it more resilient and more useful.

Vitest + smoke test suite for mock routing Simulate error responses in the mock layer for at least 5% of calls Memoize expensive admin dashboard computations In-memory store mirror + write-through persistence Deploy behind a CDN that injects security headers Automate localStorage schema version bumps
Phase 3 – Medium-term (months)

Productisation — if that is the goal

Everything required to convert the demo into an actual SaaS. Detailed breakdown lives in the SaaS Readiness report.

Real backend (Next API routes or separate service) Postgres schema + migrations + ORM (reintroduce Drizzle) NextAuth + role hierarchy + session management Real Stripe integration + webhook verification Tenant scoping in schema and query layer Email transactional layer
Phase 4 – Long-term (quarters)

Platform features

The work that lives beyond "it is a SaaS now" — monitoring, billing ergonomics, admin tooling.

Structured logging + error tracking (Sentry) Per-tenant branding and display customization Usage analytics and billing portal API keys for integrations Self-service onboarding and invitations