A news and Q&A site for developers: read and post articles, ask and answer questions, subscribe to a newsletter, and unlock a premium analytics dashboard.
It runs for anyone, with no accounts and no API keys. Clone it, run one
command, and the app comes up on http://localhost:3000 with seeded content —
no cloud project, no configuration.
Requires Node 22 or newer (the server uses the built-in node:sqlite
module) and make.
make demoThat builds the client, seeds a fresh database, and serves the whole app on
http://localhost:3000. Sign in with any seeded account — password
password123:
| Role | |
|---|---|
ada@example.com |
administrator + premium |
ravi@example.com |
premium |
mei@example.com |
regular |
There is no .env to create and nothing to sign up for. All accounts and
content are fictional.
NewsAppFinal began as a Deakin coursework app built on Firebase Auth,
Firestore, Netlify Functions and Stripe. It worked — but only for its author:
Firebase needs a live Google project whose keys are (correctly) never
committed, so a reviewer who cloned it got a blank screen.
This version replaces that cloud dependency with a local backend, so the
app is judged on what it does rather than on whether you hold the author's keys.
The original is preserved at the v0-firebase tag.
flowchart LR
subgraph Browser
UI["React 19 + Vite<br/>Tailwind"]
api["src/api.js<br/>src/auth.js"]
UI --> api
end
subgraph "Node process"
EX["Express"]
MW["JWT middleware<br/>requireAuth / requireAdmin"]
RT["routes:<br/>auth · posts · users<br/>checkout · stats · subscribe"]
DB[("node:sqlite<br/>news.db")]
EX --> MW --> RT --> DB
end
api -->|"/api/* (relative)"| EX
EX -->|"serves built client in demo"| UI
RT -. "only if a key is set" .-> STRIPE["Stripe"]
RT -. "only if a key is set" .-> MAIL["Mailchimp / SendGrid"]
The client only ever calls relative paths. In development Vite proxies them to the server; in the demo one Node process serves both the built client and the API, so the same paths work with no configuration. The external services are dashed because they are reached only when their key is present — otherwise the request runs a labelled demo path (see below).
client/ React 19 + Vite + Tailwind
src/api.js all data access — one place that talks to the server
src/auth.js session state (token + useCurrentUser hook)
src/components/ NavBar, cards, forms, route guards, error boundary
src/pages/ home, article, questions, plans, dashboard, admin, …
server/ Express + node:sqlite
server.js wiring; also serves the built client in the demo
db.js schema created on boot; ./data/news.db
seed.js demo users, articles and questions (with images)
passwords.js scrypt hashing (node:crypto)
middleware/auth.js token issuing, requireAuth, requireAdmin
routes/ auth, posts, users, checkout, stats, subscribe
tests/ API tests that drive the real server over HTTP
- Data & auth are an Express API over SQLite — the built-in
node:sqlitemodule, so there is nothing to install and nothing to compile. - Passwords are hashed with
scryptfrom Node's owncrypto. Sessions are a signed JWT. With noJWT_SECRETset, the server generates a random one per process — so no signing secret sits in the repository, and sessions simply do not survive a restart (fine for a demo; log in again). - Authorization is enforced on the server. The user list needs a valid token and an admin flag held in the database; the premium dashboard needs the premium flag. Nothing security-relevant is decided in the browser.
An article renders its banner image and Markdown body:
Signed-in users post articles and questions; the author and timestamp are set by the server from the token, never trusted from the browser:
Premium users get an analytics dashboard drawn from real database counts (not the placeholder figures the coursework version showed):
The paid integrations degrade to a clearly-labelled demo path when their key is absent — nothing throws, and the UI shows exactly what a keyed server would have sent, so you can see the integration is wired rather than stubbed.
| Integration | With no key (default) | With a key set |
|---|---|---|
| Stripe | Panel shows the real POST /v1/checkout/sessions; premium unlocks locally |
Real Stripe Checkout |
| Mailchimp | Subscriber recorded in SQLite; panel shows the real /members call |
Real list subscribe |
| SendGrid | Panel shows the real /v3/mail/send call; nothing sent |
Real welcome email |
Email addresses are masked (d***@example.com) before they reach a log or a
panel — personal data never appears in full.
make test79 tests. The 51 API tests start the real server as a process and drive it over HTTP — no production file was reshaped to be reachable. The 28 client tests run under Vitest. Each suite was validated by reintroducing every fixed defect in a throwaway copy and confirming the matching test turns red; a suite that has only ever passed proves nothing.
make checkscripts/check-secrets.sh fails the build if anything resembling a real
credential (Stripe, SendGrid, Mailchimp, a JWT secret, a Firebase key, private
keys) or real personal data (any email outside the reserved demo domains) is in
the tree. It is proven by planting a fake for each rule in a throwaway repo and
confirming each is caught — never by mutating this one. It runs in CI on every
push, alongside the test suite.
No secret has ever been committed; the working tree and full history were scanned before this was made public.
Administrators get a user-management page, gated on a server-checked admin flag:
There is none, and that is deliberate. A long-running Express process cannot run on Netlify, and SQLite does not persist on serverless functions, so keeping a live deployment would require hosted Postgres and a keyed platform account — exactly the "you need the author's keys" problem this rewrite removes. The one-command local demo is the deliverable.
Parts of this rewrite were developed with the assistance of Claude (Anthropic). All design decisions, review and testing were my own.
Daniel Allen — Bachelor of Information Technology (Application Development), Deakin University.





