LinuxDojo is a multi-platform terminal command learning platform. Users browse, search, and practice commands for Linux, Windows, macOS, and Git Bash through a guided step-by-step system. Commands are filtered by platform and category. Progress is tracked per-user via Supabase. An admin dashboard allows full CRUD management of command content.
| Layer | Technology |
|---|---|
| Frontend | HTML + CSS + Vanilla JS (ES Modules) |
| Backend | Node.js 18+ with Express 5 |
| Auth | Supabase Auth — GitHub OAuth |
| Database | Supabase Postgres (hosted) |
| Security | Supabase Row Level Security (RLS) |
| CDN deps | Supabase JS client, Google Fonts |
| Deployment | Render (Node.js Web Service) |
Browser
├── Loads HTML/JS/CSS from Express static server
├── Fetches /config.js → window.__CONFIG__ (Supabase creds injected server-side)
└── Supabase JS client (CDN)
├── Auth: GitHub OAuth (session managed by Supabase)
└── DB: Direct queries to Supabase Postgres
├── commands (content — Linux, Windows, macOS, Git Bash)
├── profiles (user roles)
└── progress (practice tracking)
Express Backend (server.js)
├── GET /config.js → serves SUPABASE_URL + SUPABASE_ANON_KEY from env vars
└── Serves /public/** as static files
The backend does NOT proxy database queries. All DB access happens from the browser via the Supabase JS client, enforced by RLS policies.
Commands are organized by platform using a tag-based convention. The first tag in the tags array identifies the platform:
| Platform Tag | Platform |
|---|---|
linux |
Linux / Ubuntu / Debian (bash) |
windows |
Windows CMD / PowerShell |
macos |
macOS Terminal (zsh/bash) |
git-bash |
Git Bash (MINGW64 on Windows) |
The home page renders platform tabs for filtering. The admin dashboard includes a required Platform dropdown that auto-injects the platform tag. Category tags (e.g., filesystem, networking) are stored alongside the platform tag in the same tags array.
linuxdojo/
├── server.js # Express: /config.js + static file server
├── package.json # Dependencies: express, dotenv
├── .env # Local env vars (not committed)
├── .gitignore
├── README.md
├── specifications.md # This file
├── scope.md
├── features.md
└── public/
├── index.html # Home: browse, search, filter commands
├── login.html # GitHub OAuth login
├── callback.html # OAuth redirect handler
├── command.html # Command detail (description, media)
├── practice.html # Guided practice mode
├── me.html # My Progress (auth required)
├── admin.html # Admin dashboard (admin role required)
└── js/
└── app.js # Shared: getSupabase, getSessionAndRole, renderNav, escapeHtml
Runtime: Node.js with "type": "module" (ES Modules)
Dependencies:
express^5.2.1 — HTTP serverdotenv^17.2.3 —.envloader
Routes:
| Method | Path | Description |
|---|---|---|
| GET | /config.js |
Returns JS that sets window.__CONFIG__ with Supabase creds |
| ALL | /* |
Serves static files from /public |
Critical ordering: /config.js route is registered before express.static. This prevents a static config.js file from shadowing the dynamic route.
Environment variables:
| Variable | Required | Description |
|---|---|---|
SUPABASE_URL |
Yes | Supabase project URL |
SUPABASE_ANON_KEY |
Yes | Supabase public anon key |
PORT |
No | HTTP port (default: 4000) |
Config injection response format:
window.__CONFIG__ = {"SUPABASE_URL":"https://xxx.supabase.co","SUPABASE_ANON_KEY":"eyJ..."};Exported functions used by all pages:
- Reads
window.__CONFIG__ - Creates and returns a Supabase JS client instance
- Throws if
window.__CONFIG__is undefined (config.js not loaded)
- Calls
supabase.auth.getSession() - Queries
profilestable forroleandemail - Returns
{ user, email, role }—roleis"admin"or"user"
- Builds navigation bar HTML
stateshape:{ loggedIn, isAdmin }- Logged-in nav: Admin (if admin), Practice, Progress, Logout
- Logged-out nav: Practice, Login
- XSS sanitization: escapes
& < > " ' - Used on all user-visible DB strings rendered as HTML
All tables live in the public schema. Supabase Auth handles auth.users.
Auto-created on first login via Supabase trigger.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| id | uuid | PK, references auth.users | Matches auth.users.id |
| text | |||
| role | text | DEFAULT 'user' | 'user' or 'admin' |
| created_at | timestamptz | DEFAULT now() |
| Column | Type | Constraints | Notes |
|---|---|---|---|
| id | uuid | PK, DEFAULT gen_random_uuid() | |
| slug | text | UNIQUE NOT NULL | URL-safe identifier, e.g. cd |
| title | text | NOT NULL | Display name |
| syntax | text | NOT NULL | e.g. cd [directory] |
| description | text | Full explanation (HTML safe) | |
| asciinema_url | text | Link or embed URL | |
| video_url | text | YouTube, Vimeo, or direct URL | |
| tags | text[] | First tag = platform (linux/windows/macos/git-bash), rest = categories | |
| lesson_steps | text[] | Ordered practice commands | |
| published | boolean | DEFAULT false | Only published cmds shown publicly |
| created_at | timestamptz | DEFAULT now() | |
| updated_at | timestamptz | Updated on every edit |
| Column | Type | Constraints | Notes |
|---|---|---|---|
| id | uuid | PK, DEFAULT gen_random_uuid() | |
| user_id | uuid | REFERENCES auth.users, UNIQUE (user_id, command_slug) | |
| command_slug | text | NOT NULL | |
| step_index | int | DEFAULT 0 | Current step (0-indexed) |
| is_completed | boolean | DEFAULT false | |
| completed_at | timestamptz | Set when is_completed=true | |
| updated_at | timestamptz |
| Table | Operation | Policy |
|---|---|---|
commands |
SELECT | published = true OR user is admin |
commands |
INSERT | role = 'admin' |
commands |
UPDATE | role = 'admin' |
commands |
DELETE | role = 'admin' |
profiles |
SELECT | id = auth.uid() |
profiles |
UPDATE | id = auth.uid() |
progress |
ALL | user_id = auth.uid() |
- GitHub OAuth via Supabase Auth
1. User visits /login.html
2. Clicks "Continue with GitHub"
3. supabase.auth.signInWithOAuth({ provider: "github", redirectTo: origin + "/callback.html" })
4. GitHub OAuth redirects to /callback.html with auth code in URL fragment
5. callback.html calls supabase.auth.getSession() — Supabase processes the fragment
6. On first login: Supabase trigger inserts row into public.profiles with role='user'
7. Session stored in browser (cookie/localStorage, managed by Supabase)
8. Redirect to /
const supabase = getSupabase();
const { user, email, role } = await getSessionAndRole(supabase);
if (!user) { window.location.href = "/login.html"; return; }
if (role !== "admin") { window.location.href = "/login.html"; return; }UPDATE public.profiles SET role = 'admin' WHERE email = 'user@example.com';Auth: Optional (used to show completed status)
Data fetched:
SELECT * FROM commands WHERE published = true(always)SELECT command_slug FROM progress WHERE user_id = $uid AND is_completed = true(if logged in)
UI Components:
- Hero: title, description, stats (total commands, completed, categories)
- Platform tabs: All / Linux / Windows / macOS / Git Bash (filters by platform tag)
- Search bar: real-time filter on title, slug, syntax
- Tag filter chips: click to filter by category tag (platform tags excluded from chips)
- Command grid: cards with title, syntax, slug, platform badge, tags, difficulty, "Done" badge
Auth: Optional
Data fetched:
SELECT * FROM commands WHERE slug = $slug AND published = true
UI Components:
- Title + syntax badge
- Tags list
- Description
- Asciinema embed (normalized URL → iframe)
- Video embed (YouTube / Vimeo / direct video)
- "Practice" button →
/practice.html?slug=<slug>
Auth: Optional (progress saved to localStorage; synced to DB if logged in)
Data fetched:
SELECT * FROM commands WHERE slug = $slugSELECT * FROM progress WHERE user_id = $uid AND command_slug = $slug(if logged in)
Data written:
- UPSERT into
progress(step_index, is_completed, completed_at)
UI Components:
- Left panel: step list, current step highlight, progress pill, DB sync status
- Right panel: textarea, Check / Clear buttons, feedback message, hint box
- Validation: normalizes whitespace, checks last non-empty line
Auth: Required (redirects to /login.html if not logged in)
Data fetched:
SELECT * FROM progress WHERE user_id = $uidSELECT title, syntax, slug FROM commands WHERE slug IN ($slugs)
UI Components:
- Completed commands card (is_completed = true)
- In-progress commands card (step_index > 0, is_completed = false)
Auth: Required, role = 'admin'
Data fetched/written:
- Full CRUD on
commandstable
UI Components:
- Left: form (all command fields + platform dropdown + publish toggle)
- Right: live asciinema preview, live video preview, commands list with platform badges and action buttons
Auth: None required
UI: GitHub OAuth button, sign-out button (if already logged in), info box
Auth: Processes session from URL fragment
Logic: supabase.auth.getSession() → success redirect to / or error message
- Input:
https://asciinema.org/a/<id>or embed URL - Normalized to:
https://asciinema.org/a/<id>/iframe - Rendered as:
<iframe>with fixed dimensions
- YouTube: URL → embed URL (
/embed/<id>) in<iframe> - Vimeo: URL → embed URL (
https://player.vimeo.com/video/<id>) in<iframe> - Direct video (
.mp4,.webm,.ogg):<video>tag
| Concern | Mitigation |
|---|---|
| XSS | escapeHtml() on all DB strings rendered as HTML |
| SQL injection | Supabase JS client uses parameterized queries |
| Credential leak | .env excluded from git; creds served via /config.js |
| Unauthorized DB writes | RLS policies on all tables |
| Admin bypass | Role checked in DB (profiles.role), not just client |
| Auth spoofing | Session managed by Supabase Auth (JWT-based) |
- Connect GitHub repo as Node.js Web Service
- Build command:
npm install - Start command:
node server.js - Environment variables:
SUPABASE_URL,SUPABASE_ANON_KEY - Supabase OAuth redirect URLs: add
https://<render-domain>/callback.html
# 1. Create .env in project root
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your-anon-key
PORT=4000
# 2. Install and run
npm install
npm run dev
# → http://localhost:4000{
"dependencies": {
"express": "^5.2.1",
"dotenv": "^17.2.3"
}
}Frontend CDN:
https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/dist/umd/supabase.min.js- Google Fonts (Inter)