A self-hosted, open-source course community platform that integrates with Brightspace (and other LTI 1.3-compatible LMSs). Goes beyond a discussion board — it's a full community environment for communication, collaboration, co-creation, peer recognition, and structured peer feedback.
| Feature | Description |
|---|---|
| 💬 Threaded Discussions | Nested replies, markdown, reactions, instructor notes |
| ❓ Q&A Board | Upvoting, accepted answers, unresolved question tracking |
| 🧩 Collaboration Boards | Drag-and-drop sticky notes for visual thinking |
| 📚 Resource Library | Community-curated links with previews |
| 🎉 Kudos | Peer recognition posts |
| 🪞 Reflections | Post type for metacognitive writing |
| 📊 Polls | Anonymous check-ins with live results |
| 📊 Community Pulse | Instructor analytics: engagement, contributors, silent students |
| 📡 Pulse Checks | Live audience response — multiple choice, ratings, word clouds, short text; QR code for public sessions |
| 🔔 Notifications | Real-time reply and mention alerts |
| 🔁 Peer Feedback | Full anonymous peer review workflow with file uploads |
| 🛡️ Community Moderation | Graduated flag-and-review system with audit log |
| 🔒 Admin Panel | Course management, backup & restore, file cleanup |
git clone <repo>
cd course-community
# Set dev mode (bypasses LTI auth, creates sample data)
export DEV_MODE=true
export APP_URL=http://localhost:8080
php -S localhost:8080
# Open in browser:
# http://localhost:8080/lti.php?action=dev ← logs in as Dev Instructor
# http://localhost:8080/ ← public landing page when unauthenticatedDev mode creates a sample instructor and student, seeds 5 posts across spaces.
- PHP 8.1+ with extensions:
pdo_sqlite,openssl,json,mbstring,zip(for backup/restore) - SQLite 3 (bundled with PHP)
- HTTPS required for production LTI use
- Apache with
mod_rewriteenabled, or Nginx
- Copy all files to your web server's document root (or a subdirectory)
- Make
data/writable:chmod 755 data/ - Configure
config.phpor set environment variables (see below) - Register the tool in your LMS (see LTI Setup)
- Set an admin password in
config.phpor viaADMIN_PASSWORDenv var - Visit
/admin.phpto access the admin panel
Brightspace requires four steps: Register → Deploy → Create a Link → Add to course.
Replace https://your-server.com with your actual tool URL throughout.
In Brightspace: Admin Tools → External Learning Tools → Register a Tool
| Field | Value |
|---|---|
| Domain | your-server.com |
| Redirect URLs | https://your-server.com/lti.php?action=launch |
| OpenID Connect Login URL | https://your-server.com/lti.php?action=login |
| Target Link URI | https://your-server.com/ |
| Keyset URL | (leave blank — tool doesn't initiate back-channel requests) |
After saving, Brightspace shows you a Client ID, an auth endpoint, and a JWKS URI. Copy all three — you'll need them for config.php.
$LTI_PLATFORMS = [
'https://your.brightspace.com' => [ // issuer — your Brightspace hostname
'client_id' => 'PASTE_CLIENT_ID', // from Step 1
'auth_endpoint' => 'https://your.brightspace.com/d2l/lti/authenticate',
'jwks_uri' => 'https://your.brightspace.com/d2l/.well-known/jwks',
],
];The auth_endpoint and jwks_uri patterns are always the same — just substitute your institution's Brightspace hostname.
In Brightspace: Admin Tools → External Learning Tools → find Course Community → Deploy
Security Settings — check the following:
| Checkbox | Share? |
|---|---|
| Name (First Name + Last Name) | ✅ Yes |
| ✅ Yes | |
| Middle Name, User ID, Username, Org Defined Id | ☐ No |
| Classlist including users not known to this deployment | ☐ No |
Configuration Settings — leave all unchecked (no grade passback, no external resource mode).
Substitution Parameters / Custom Parameters — leave both empty.
Set the deployment scope to org-wide (or to the specific org units that need it) so the tool is visible in courses.
Without a Link, the tool won't appear in the course activity picker.
In Brightspace: open the deployment you just created → find the Links tab → add a new link:
| Field | Value |
|---|---|
| Title | Course Community |
| URL | https://your-server.com/ |
| Type | Basic Launch |
- Inside your Brightspace course, go to Content
- Choose Add Existing Activities → External Learning Tools
- Find Course Community in the list and select it
That creates the launch link. When anyone clicks it, Brightspace runs the LTI 1.3 OIDC flow and they arrive in Course Community already authenticated with their name, email, and role.
First launch tip: Have an instructor open the tool first. Instructors get course-management permissions (pinning posts, creating peer feedback assignments, etc.) that students don't.
$LTI_PLATFORMS = [
'https://your.brightspace.com' => [
'client_id' => 'your-client-id-from-brightspace',
'auth_endpoint' => 'https://your.brightspace.com/d2l/lti/authenticate',
'jwks_uri' => 'https://your.brightspace.com/d2l/.well-known/jwks',
],
];You can register any number of LMS instances. Each entry is either a plain issuer URL (one registration per LMS) or a compound "issuer::client_id" key (multiple registrations per LMS):
$LTI_PLATFORMS = [
// One registration from one Brightspace instance
'https://uni-a.brightspace.com' => [
'client_id' => 'abc123',
'auth_endpoint' => 'https://uni-a.brightspace.com/d2l/lti/authenticate',
'jwks_uri' => 'https://uni-a.brightspace.com/d2l/.well-known/jwks',
],
// Second institution
'https://uni-b.brightspace.com' => [
'client_id' => 'xyz789',
'auth_endpoint' => 'https://uni-b.brightspace.com/d2l/lti/authenticate',
'jwks_uri' => 'https://uni-b.brightspace.com/d2l/.well-known/jwks',
],
// Two separate registrations from the same LMS (use compound key)
'https://uni-c.brightspace.com::prod-client' => [
'client_id' => 'prod-client',
'auth_endpoint' => 'https://uni-c.brightspace.com/d2l/lti/authenticate',
'jwks_uri' => 'https://uni-c.brightspace.com/d2l/.well-known/jwks',
],
'https://uni-c.brightspace.com::test-client' => [
'client_id' => 'test-client',
'auth_endpoint' => 'https://uni-c.brightspace.com/d2l/lti/authenticate',
'jwks_uri' => 'https://uni-c.brightspace.com/d2l/.well-known/jwks',
],
];Instead of editing config.php, set these environment variables (useful for containers/CI):
APP_URL=https://your-server.com
LTI_ISSUER=https://your.brightspace.com
LTI_CLIENT_ID=12345678
LTI_AUTH_ENDPOINT=https://your.brightspace.com/d2l/lti/authenticate
LTI_JWKS_URI=https://your.brightspace.com/d2l/.well-known/jwks
ADMIN_PASSWORD=your-secure-admin-password
Upload status.php and visit it in your browser before attempting a launch. It checks PHP version, required extensions, data/ writability, APP_URL configuration, SQLite access, and HTTPS. Delete it once you've resolved any issues — it exposes server details that should not be public.
These appear in your PHP error log tagged [LTI] or [API]. Set CC_DEBUG=true as an environment variable to show the real message in the browser instead of the generic error page.
| Log message | Cause | Fix |
|---|---|---|
unknown function: unixepoch() |
SQLite < 3.38.0 on server; old database schema stored with unixepoch() defaults |
Delete data/community.sqlite and re-upload db.php — the database will be recreated with compatible strftime('%s','now') defaults |
Failed to fetch JWKS from … |
allow_url_fopen is disabled in php.ini |
Enable allow_url_fopen in php.ini or ask your host |
Unregistered platform: https://… |
The issuer URL in $LTI_PLATFORMS doesn't exactly match what Brightspace sends |
Copy the exact issuer from the Brightspace registration page into config.php |
JWT aud mismatch |
client_id in config doesn't match the token |
Double-check the Client ID copied from Brightspace Step 1 |
Invalid or expired state |
The OIDC state cookie expired or was lost | Usually caused by the browser blocking cross-site cookies in an iframe — try opening the tool in a new tab |
near "NULLS": syntax error |
SQLite < 3.30.0 doesn't support NULLS LAST |
Ensure you have the latest api.php |
SQLSTATE … General error: 1 … |
Database not writable, or wrong PHP/SQLite version | Run status.php to identify the specific issue |
These appear as error pages shown by Brightspace or the browser during the login flow.
| Error | Cause | Fix |
|---|---|---|
| "Invalid authentication request parameters" | The redirect_uri sent to Brightspace doesn't match the registered Redirect URL — almost always because APP_URL is wrong or unset |
Set APP_URL=https://your-server.com/path as an environment variable or in config.php; the value must match the subdirectory the app is deployed in |
500 Internal Server Error on lti.php?action=login |
PHP fatal error before any output — common causes: data/ not writable, PHP < 8.1, missing pdo_sqlite extension |
Run status.php to identify; set CC_DEBUG=true to see the error in the browser |
| "LTI authentication failed. Please try relaunching." | An exception was thrown during the launch — see PHP error log for [LTI] entry |
Set CC_DEBUG=true temporarily to see the real message in the browser |
| Tool not appearing in Add Existing Activities → External Learning Tools | The Link step (Step 4) was skipped, or the deployment scope doesn't include this course's org unit | Create a Link on the deployment; check the deployment's org unit scope |
| Blank page / CSS loads but JS errors | APP_URL subdirectory is set but asset paths or API calls are hitting the wrong URL |
Ensure APP_URL includes the full subdirectory path, e.g. https://server.com/course-community not just https://server.com |
| Error | Cause | Fix |
|---|---|---|
MIME type ("text/html") blocked for app.js or style.css |
Asset paths don't include the subdirectory — APP_URL not set correctly |
Set APP_URL to the full path including subdirectory |
selectSpace is not defined / submitPost is not defined |
Function not exported to window scope from ES module |
Ensure you have the latest assets/app.js |
| API calls returning 404 or HTML | baseUrl missing from APP_CONFIG — API requests go to domain root instead of app subdirectory |
Ensure APP_URL is set; upload the latest index.php and api.php |
APP_URL must be set to the exact base URL of the app with no trailing slash. Every broken redirect, wrong asset path, and Brightspace auth rejection in a subdirectory deployment traces back to this.
Apache .htaccess (simplest for shared hosting):
SetEnv APP_URL https://your-server.com/course-communityPHP-FPM / server php.ini:
env[APP_URL] = https://your-server.com/course-communityShell / Docker:
export APP_URL=https://your-server.com/course-communityEvery course context is fully sandboxed:
- Courses are identified by a composite key of
(issuer, context_id)— two different LMS instances with the samecontext_idvalue never share a course record. - All content (posts, comments, boards, peer feedback, notifications) is scoped to a
course_id. - Session tokens are bound to a
course_id; users cannot access data from other courses even within the same browser session. - LTI
client_idvalues are validated against the registered platform config — the tool rejects logins that present an unregistered client.
Instructors can create structured peer review assignments within any course.
- Instructor creates assignment — sets title, description, custom review prompts, number of reviewers per submission, and whether to accept text and/or file uploads.
- Instructor opens assignment — students can now submit their work (text and/or file).
- Instructor triggers assignment — the tool automatically assigns each submission to reviewers using a load-balancing algorithm (no student reviews their own work; workload is distributed evenly).
- Students complete reviews — each reviewer responds to the prompts and submits feedback.
- Instructor closes assignment — feedback is released to authors.
| Phase | Description |
|---|---|
draft |
Visible to instructors only; not yet open for submissions |
open |
Students can submit their work |
reviewing |
Reviewers have been assigned; review forms are active |
closed |
Feedback is visible to authors |
- Bidirectional anonymity: reviewers cannot see the author's name; authors cannot see who reviewed them. Instructors see all.
- File access control: uploaded files are stored outside the web root (
data/uploads/) and served only through a permission-checked API endpoint. Only the file's author, their assigned reviewers, and instructors can download a file. - Load-balanced assignment: the assignment algorithm distributes review workload as evenly as possible across the cohort.
| Setting | Default | Description |
|---|---|---|
reviewers_per_sub |
2 | How many students review each submission |
allow_text |
true | Accept inline text submissions |
allow_files |
false | Accept file uploads |
accepted_types |
pdf,doc,docx |
Allowed file extensions (comma-separated) |
max_file_mb |
10 | Maximum file size in megabytes |
prompts_json |
[] |
Array of review prompt strings shown to reviewers |
Live audience response sessions — run during class, a workshop, or a conference. Instructors create a set of questions, open them one at a time, and choose when to reveal results.
| Type | Input | Results display |
|---|---|---|
choice |
Select one option | Bar chart (count + %) |
rating |
Tap a number on a scale | Histogram + mean |
wordcloud |
One word or short phrase | Weighted word cloud |
text |
Free text (max 500 chars) | Scrolling list of responses |
- Instructor creates a check — title, question set, and access level (course or public).
- Activate — the check becomes visible to enrolled students. Public checks also generate a short URL (
/p/XXXXXXXX) and QR code. - Open questions one at a time — students and audience members see each question and respond.
- Reveal results — the instructor decides when results are visible to respondents.
- Close — no new responses accepted. Results remain visible.
| Course only | Public | |
|---|---|---|
| Who can respond | Enrolled students (requires login) | Anyone with the URL or QR code — no login |
| Share | Badge appears in student sidebar | Short URL + auto-generated QR code in presenter view |
| Deduplication | One response per student per question (updates on re-submit) | Fully anonymous — no tracking, no deduplication |
| Best for | In-class checks, exit tickets, Likert surveys | Conference talks, open lectures, workshops |
- Authenticated responses are deduplicated server-side — re-submitting updates the existing response rather than adding a duplicate.
- Public/anonymous responses have no server-side deduplication; a browser-side localStorage flag prevents accidental double-submission in the UI only.
- Response data is scoped to the course and never exposed cross-course.
A graduated, community-assisted moderation system designed to keep the space respectful without turning the instructor into a surveillance authority.
- Any student can flag a post or comment — selecting a reason (inappropriate, harassment, spam, or off-topic) and an optional note. Flags are private; the flagged author is notified and can self-correct.
- Instructors review flagged content in the Moderation panel (🛡️ in the sidebar, visible only to instructors). The queue shows flag counts, reasons, and content excerpts — sorted by flag count and recency.
- Instructors choose a graduated response:
- Send Note — private message to the author; no content change
- Hide — removes the post/comment from view; author sees a private explanation
- Redact — replaces the content with a neutral placeholder; original is preserved server-side for the instructor's reference
- Restore — reverses hide or redact, returning content to normal
- Dismiss Flags — closes the flags without taking action (false alarm, already resolved, etc.)
- Every action is logged in a full audit trail, viewable in the Moderation panel.
| Viewer | Hidden/Redacted content |
|---|---|
| Other students | Neutral placeholder: "This content has been reviewed and is not currently visible." |
| Content author | Still sees their own content + an amber notice with the instructor's explanation |
| Instructor | Sees full content with status badge and flag count |
When a single piece of content accumulates 3 or more open flags, all instructors in the course receive a notification prompting them to review it.
- Flag reports are anonymous to other students
- Authors cannot see who flagged their content
- Flag reasons and details are only visible to instructors
Access the admin panel at /admin.php. Login uses the ADMIN_PASSWORD set in config.php or the environment.
- Dashboard — overview of all courses across all LMS platforms, active sessions, database size, upload storage used
- Delete course — permanently removes a course and all its data (posts, comments, boards, peer feedback, uploaded files, notifications, enrollments, sessions)
- Backup — downloads a
.ziparchive containing the SQLite database and all peer feedback uploads - Restore — uploads a backup
.zipto replace the current database and uploads
Note: Restore replaces the live database. Take a fresh backup before restoring. The PHP
zipextension must be installed for backup/restore to work.
To set the admin password, add this to config.php or set the ADMIN_PASSWORD environment variable:
define('ADMIN_PASSWORD', 'your-secure-password-here');If ADMIN_PASSWORD is empty or not set, the admin panel is disabled.
Built around D'Arcy Norman's five-dimensional course design framework (The Teaching Game):
- Player — Multiple contribution modes: post, reply, curate, recognize, reflect, submit, review
- Performance — Votes, reactions, accepted answers, and structured rubrics make quality visible
- Narrative — The feed tells the story of the community over time; peer feedback captures the arc of learning
- Environment — Purposeful spaces (discussion, Q&A, boards, peer review) with distinct social norms
- System — Transparent roles, moderation tools, LTI-grounded isolation, admin oversight
course-community/
├── config.php ← LTI credentials, admin password, app settings
├── db.php ← Database layer (SQLite schema + helpers + migrations)
├── lti.php ← LTI 1.3 OIDC authentication handler
├── api.php ← JSON REST API (all endpoints)
├── index.php ← App shell / SPA (serves landing page to unauthenticated visitors)
├── landing.php ← Public info/integration guide (included by index.php)
├── pulse-public.php ← Public pulse check page (no auth, served at /p/{token})
├── admin.php ← Admin panel (protected by ADMIN_PASSWORD)
├── thumbnail.png ← 800×800 app thumbnail
├── .htaccess ← Apache rewrite rules + security headers
├── assets/
│ ├── style.css ← "Warm Commons" design system & component styles
│ └── app.js ← Frontend SPA (vanilla ES2022, no build step)
└── data/ ← Created automatically; must be writable
├── .htaccess ← Blocks direct web access to data/
├── community.sqlite← Auto-created SQLite database
└── uploads/ ← Peer feedback file uploads (served via API only)
courses — one row per (issuer, context_id) pair
users — one row per (sub, issuer) pair
enrollments — user ↔ course membership + role
spaces — discussion channels within a course
posts — content items (discussion, Q&A, kudos, reflection, poll, resource)
comments — threaded replies on posts
reactions — emoji reactions on posts/comments
votes — upvotes on posts/comments
tags / post_tags — tagging system
boards — collaboration boards
board_cards — sticky notes on boards
poll_votes — anonymous poll responses
notifications — per-user, per-course alerts
sessions — authenticated session tokens
lti_states — OIDC login state (short-lived)
lti_nonces — replay-attack prevention (short-lived)
pf_assignments — peer feedback assignments (per course)
pf_submissions — student work submissions
pf_review_assignments — reviewer-to-submission mapping
pf_responses — completed review responses
flags — content reports (one per user per item)
moderation_log — full audit trail of instructor moderation actions
pulse_checks — live response sessions (title, access, status, share_token)
pulse_questions — questions within a check (type, options, open/reveal flags)
pulse_responses — individual responses (user_id NULL for anonymous/public)
MIT — use it, adapt it, improve it.