A learning-focused web app to master terminal commands across multiple platforms:
Supported Platforms: Linux, Windows (CMD/PowerShell), macOS, Git Bash
- Command browser with search, platform filter, and category filters
- Asciinema terminal demos + video explanations
- Guided practice mode with step-by-step lessons
- Per-user progress tracking (completed / in-progress)
- Admin dashboard to create, edit, and publish commands
Built with static HTML + CSS on the frontend, Node.js (Express) as the server, and Supabase for the database, auth, and security.
Documentation:
specifications.md— Full technical spec (architecture, DB schema, security, pages)scope.md— What is and isn't in scope, constraints, success criteriafeatures.md— Current features with workflows + full future feature roadmap
| Layer | Technology |
|---|---|
| Frontend | HTML + CSS + Vanilla JS (ES Modules) |
| Backend | Node.js + Express |
| Auth | Supabase Auth (GitHub OAuth) |
| Database | Supabase Postgres |
| Security | Supabase Row Level Security (RLS) |
| Deployment | Render |
LinuxDojo supports commands across four platforms. Each command is tagged with its platform, and users can filter by platform on the home page.
| Platform | Tag | Description |
|---|---|---|
| Linux | linux |
Ubuntu/Debian shell commands (bash) |
| Windows | windows |
CMD and PowerShell commands |
| macOS | macos |
macOS Terminal commands (zsh/bash) |
| Git Bash | git-bash |
Git Bash on Windows (MINGW64 environment) |
Platform is stored as a tag in the tags array on each command. The admin dashboard includes a dedicated Platform dropdown to ensure every command is tagged with its platform.
- Browse all published commands on the home page
- Filter by platform (Linux / Windows / macOS / Git Bash)
- Search by name, slug, or syntax; filter by category tag
- Open a command to view description, asciinema demo, and video
- Practice commands in guided step-by-step mode
- Track completed and in-progress commands on
/me.html
- Admin-only access to
/admin.html(role check via Supabase RLS) - Create, edit, publish/unpublish, and delete commands
- Select platform (Linux / Windows / macOS / Git Bash) per command
- Live preview for asciinema embed and video embed
linuxdojo/
├── public/
│ ├── index.html # Home: browse, search, platform filter, category filter
│ ├── command.html # Command detail: description, media, platform badge
│ ├── practice.html # Guided practice mode
│ ├── me.html # My Progress (auth required)
│ ├── login.html # GitHub OAuth sign-in
│ ├── callback.html # OAuth redirect handler
│ ├── admin.html # Admin dashboard (admin role required)
│ └── js/
│ └── app.js # Shared: getSupabase, getSessionAndRole, renderNav, escapeHtml
├── server.js # Express: serves /config.js dynamically + static files
├── package.json
├── .env # Create this in the project root for local dev (see below)
└── .gitignore # .env is excluded from git
Browser requests /config.js
|
v
server.js: GET /config.js route (runs BEFORE static middleware)
| Reads SUPABASE_URL, SUPABASE_ANON_KEY from env vars
| Returns: window.__CONFIG__ = { SUPABASE_URL, SUPABASE_ANON_KEY }
v
Browser: window.__CONFIG__ is available
|
v
public/js/app.js: getSupabase() reads window.__CONFIG__ -> creates Supabase client
The
/config.jsroute in server.js is registered beforeexpress.staticso it always serves the dynamic version from environment variables, not a hardcoded static file.
The frontend communicates directly with Supabase via the Supabase JS client (CDN). The Express server only serves files and the config -- it does not proxy database queries.
User action (search, practice, login, platform filter, etc.)
|
v
Browser: Supabase JS client (from CDN)
| Authenticated via GitHub OAuth session cookie (managed by Supabase)
| All writes/reads go through RLS policies
v
Supabase Postgres
|-- commands -- Command content for all platforms (read: public; write: admin RLS)
|-- profiles -- One row per user, stores role (read: own row only)
+-- progress -- Per-user step tracking (read/write: own rows only via RLS)
1. User clicks "Login" on /login.html
2. Browser calls supabase.auth.signInWithOAuth({ provider: "github", redirectTo: window.location.origin + "/callback.html" })
3. GitHub OAuth -> redirects back to /callback.html
4. callback.html calls supabase.auth.getSession() -- session is auto-stored by Supabase
5. On success: redirects to / (home page)
6. Supabase trigger creates a row in public.profiles on first login
7. app.js getSessionAndRole() reads profiles.role for admin check
Home page loads all published commands
|
v
User clicks platform tab (Linux / Windows / macOS / Git Bash / All)
|
v
Client-side filter: commands where tags include the selected platform
|
v
Filtered commands rendered in the list
| (tag chips also exclude platform tags to avoid duplication)
v
User can further filter by category tag and search text
/ Home page -- browse + search + platform filter
|
|-- /command.html?slug=cd Command detail (description, asciinema, video, platform badge)
| +-- /practice.html?slug=cd Guided practice for that command
|
|-- /me.html My Progress (auth required -- redirects to login if not)
|-- /login.html GitHub OAuth login
|-- /callback.html OAuth redirect handler (auto-redirects to /)
+-- /admin.html Admin dashboard (admin role required, platform dropdown)
profiles -- one row per authenticated user (auto-created by trigger)
id uuid PRIMARY KEY (references auth.users)
email text
role text DEFAULT 'user' -- 'user' | 'admin'
created_at timestamptzcommands -- command content (all platforms)
id uuid PRIMARY KEY DEFAULT gen_random_uuid()
slug text UNIQUE NOT NULL
title text NOT NULL
syntax text NOT NULL
description text
asciinema_url text
video_url text
tags text[] -- includes platform tag (linux/windows/macos/git-bash) + category tags
lesson_steps text[]
published boolean DEFAULT false
created_at timestamptz
updated_at timestamptzprogress -- per-user practice tracking
id uuid PRIMARY KEY DEFAULT gen_random_uuid()
user_id uuid REFERENCES auth.users (UNIQUE with command_slug)
command_slug text
step_index int DEFAULT 0
is_completed boolean DEFAULT false
completed_at timestamptz
updated_at timestamptzPlatform is stored as the first tag in the tags array. The admin dashboard enforces this via a required Platform dropdown. Valid platform tags:
| Tag | Platform |
|---|---|
linux |
Linux / Ubuntu / Debian (bash) |
windows |
Windows CMD / PowerShell |
macos |
macOS Terminal (zsh/bash) |
git-bash |
Git Bash (MINGW64 on Windows) |
UPDATE public.profiles
SET role = 'admin'
WHERE email = 'YOUR_EMAIL_HERE';
-- Verify
SELECT email, role FROM public.profiles;Create a .env file in the project root (same folder as server.js and package.json):
linuxdojo/
├── .env
├── server.js
└── package.json
SUPABASE_URL=https://YOUR_PROJECT_ID.supabase.co
SUPABASE_ANON_KEY=YOUR_ANON_PUBLIC_KEY
PORT=4000
SUPABASE_ANON_KEYis the public anon key -- safe to expose in client-side code. The server serves it to the browser via/config.js..envis listed in.gitignoreand will never be committed.
npm install
node server.js
# or
npm run devApp runs at: http://localhost:4000
| URL | Description |
|---|---|
/ |
Home -- command list, search, platform filter |
/login.html |
GitHub OAuth sign-in |
/callback.html |
OAuth redirect handler |
/command.html?slug=<slug> |
Command detail page (with platform badge) |
/practice.html?slug=<slug> |
Guided practice for a command |
/practice.html |
Free practice mode (no slug) |
/me.html |
My Progress (login required) |
/admin.html |
Admin dashboard (admin role required) |
/config.js |
Dynamic config endpoint (server-side) |
-
Connect your GitHub repo to Render (Web Service)
-
Build command:
npm install -
Start command:
node server.js -
Add environment variables in the Render dashboard:
SUPABASE_URLSUPABASE_ANON_KEYPORT(Render sets this automatically)
-
Update Supabase OAuth redirect URLs (in Supabase dashboard -> Authentication -> URL Configuration):
- Add
https://YOUR_RENDER_DOMAIN/callback.html - Add
https://YOUR_RENDER_DOMAINas Site URL
- Add
Note: The OAuth
redirectToURL is set dynamically usingwindow.location.origininlogin.html, so it works automatically on both localhost and production -- no code change needed.
Educational / personal project. All rights reserved.