chore(docs): migrate the docs site from Fumadocs to Blume (#753)

* Update documentation for ClassroomIO: change Fumadocs to Blume, revise MDX format conventions, and clarify sidebar navigation requirements.

* docs: update README and scripts for improved preview and error handling

* docs: update Discord link in configuration and documentation

* docs: add token-auth route and update documentation formatting

* Fix formatting in auto-comment-issues.yml

* Fix JSON syntax by adding a comma

* fix(docs): declare Blume's Astro build deps so Cloudflare build resolves them

blume build generates an Astro project under apps/docs/.blume/ that imports
astro, @astrojs/mdx, @tailwindcss/vite, tailwindcss, @tailwindcss/typography,
@shikijs/twoslash and @scalar/astro. Under pnpm's isolated node_modules these
are nested inside blume and not resolvable from the docs app, so a clean
--frozen-lockfile build (Cloudflare) fails at 'Cannot find module astro/config'.
Declare them directly in @cio/docs, pinned to blume's resolved versions.

* refactor(docs): update API reference paths and documentation for clarity

* fix: change platofrm url to docs home page

* Refactor documentation for self-hosting ClassroomIO

- Removed the Supabase Cloud setup guide as it is no longer needed.
- Updated the Enterprise SSO documentation to include a reference link for SSO & licensing.
- Added new documentation for Content Security Policy (CSP) configuration.
- Created a new section for configuring SMTP for email notifications.
- Introduced a comprehensive configuration index for self-hosted ClassroomIO, detailing environment variables and their usage.
- Added documentation for optional integrations including Google OAuth, OpenAI, and Unsplash.
- Documented the generation and usage of required secrets for ClassroomIO.
- Created a section for SSO & licensing, detailing the requirements for Enterprise features.
- Added object storage configuration documentation, covering both bundled MinIO and external S3-compatible services.
- Documented versioning and upgrade processes for ClassroomIO.
- Updated the Coolify and Docker self-hosting guides to streamline configuration references and upgrade instructions.
- Enhanced the Railway self-hosting guide with links to relevant configuration sections.
- Updated token authentication documentation to reference SSO & licensing.

* Add comprehensive guides for ClassroomIO features and functionalities

- Introduced a new guide for the Student Dashboard, detailing navigation and features available to students.
- Added a guide on using Tags for organizing and filtering courses within ClassroomIO.
- Created a guide on how to take attendance in courses, including marking attendance and related functionalities.
- Developed a Terminology guide to clarify key terms used throughout ClassroomIO.
- Implemented a Token Auth guide for signing users into ClassroomIO from external applications using JWT.
- Added instructions for using math in the editor with LaTeX support.
- Created a guide for customizing the welcome email sent to students upon enrollment in courses.
- Added a guide explaining what ClassroomIO is and its key features.
- Updated the home documentation to provide a clearer overview of ClassroomIO and its resources.
- Enhanced self-hosted configuration documentation with a new guide for deploying ClassroomIO on Dokploy.
- Updated links in the self-hosted configuration documentation for better navigation.
- Added new images to support the Dokploy deployment guide.
- Updated asset packaging script to handle redirects for the new guide structure.
- Fixed footer link to point to the correct Getting Started guide.

* chore: uprage version

* feat(docs): add DigitalOcean deployment guide and flatten guides structure

- Add a guide for deploying ClassroomIO on DigitalOcean using Docker Compose
  and Caddy for automatic HTTPS.
- List DigitalOcean as a deployment option in the self-hosting index.
- Move guides out of content/docs/guides into content/docs (student-dashboard,
  tags, terminology, token-auth, what-is-classroomio, audience, cohorts,
  community, course, enterprise-sso, mcp, organization, quickstart/*).
- Rename home.mdx to index.mdx.
- Update package-assets script to emit redirects for the old guides paths.

---------

Co-authored-by: Rotimi Best <best@classroomio.com>
This commit is contained in:
Abdulmuizz Hamzat
2026-07-22 22:30:10 +01:00
committed by GitHub
co-authored by Rotimi Best
parent 5efd7a38c8
commit b599f1a017
111 changed files with 6819 additions and 3646 deletions
+1 -1
View File
@@ -20,4 +20,4 @@ jobs:
issue-number: ${{ github.event.issue.number }}
body: |
Thanks for the issue, our team will look into it as soon as possible. If you would like to work on this issue, please wait for us to decide if it's ready. To claim an issue that does not have the "needs triage" label, please leave a comment that says ".take". If you have any questions, please reach out to us on [Discord](https://discord.gg/KJUb2AwM) or follow up on the issue itself.
For full info on how to contribute, please check out our [contributors guide](https://classroomio.com/docs/contributorguides/devs)
For full info on how to contribute, please check out our [contributors guide](https://classroomio.com/docs/contributor-guides/devs).
+68
View File
@@ -0,0 +1,68 @@
name: Deploy Docs
on:
push:
branches: [main]
paths:
- 'apps/docs/**'
- 'packages/utils/src/openapi/**'
# Called by upload-openapi-spec.yml: the API reference is rendered statically
# at build time, so a new spec in R2 only reaches the site via a docs rebuild.
workflow_call:
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- name: Checkout code
uses: actions/checkout@v4
# Blume requires Node >= 22.12; the rest of the monorepo is still on 20.
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: 'apps/docs/.nvmrc'
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Get pnpm store directory
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
# scripts/fetch-openapi.mjs imports @cio/utils/openapi/public-api from dist.
- name: Build workspace dependencies
run: pnpm turbo run build --filter=@cio/docs^...
# Reports broken internal links and heading anchors. Advisory rather than
# a gate, because the checker is base-unaware and always reports two known
# classes of false positive: links into the /api reference (Scalar's route
# is not in the page index) and every image under public/ (it resolves public/
# against the site root, which deployment.base '/docs' moves). Read the log
# for anything outside those two.
- name: Validate links and anchors
run: pnpm --filter @cio/docs validate
continue-on-error: true
- name: Build docs
run: pnpm --filter @cio/docs build
- name: Deploy to Cloudflare
run: pnpm --filter @cio/docs exec wrangler deploy
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
@@ -70,3 +70,10 @@ jobs:
CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }}
PUBLIC_SUPABASE_URL: ${{ secrets.PUBLIC_SUPABASE_URL }}
PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.PUBLIC_SUPABASE_ANON_KEY }}
# The docs render the API reference statically from this spec, so a fresh
# upload only reaches classroomio.com/docs/api once they rebuild.
redeploy-docs:
needs: generate-and-upload-openapi
uses: ./.github/workflows/deploy-docs.yml
secrets: inherit
+1 -1
View File
@@ -250,7 +250,7 @@ cp .env.example .env # copy env template, edit for your domain
The script reads root `.env` via `docker compose --env-file .env` and auto-generates a secure `PRIVATE_SERVER_KEY` (and `BETTER_AUTH_SECRET`) when missing.
See [`.env.example`](.env.example) for the full list of environment variables with required/optional grouping, and [`docker/docs/SELF_HOST.md`](docker/docs/SELF_HOST.md) for the complete Docker self-hosting guide. To raise upload caps (documents, videos, assignment files), set the `UPLOAD_MAX_*_MB` vars in `.env` — see the [docs](https://docs.classroomio.com/docs/self-hosted/docker#optional--upload-file-size-limits).
See [`.env.example`](.env.example) for the full list of environment variables with required/optional grouping, and [`docker/docs/SELF_HOST.md`](docker/docs/SELF_HOST.md) for the complete Docker self-hosting guide. To raise upload caps (documents, videos, assignment files), set the `UPLOAD_MAX_*_MB` vars in `.env` — see the [docs](https://classroomio.com/docs/self-hosted/docker#optional--upload-file-size-limits).
## Publishing
@@ -492,7 +492,7 @@
<Field.Description>
{$t('course.navItem.settings.course_type_desc')}
<a
href="https://classroomio.com/docs/course-types"
href="https://classroomio.com/docs/guides/course-types"
target="_blank"
rel="noopener noreferrer"
class="ui:text-primary underline"
@@ -26,7 +26,7 @@
<Page.Subtitle>{$t('automation.api.subtitle')}</Page.Subtitle>
</Page.HeaderContent>
<Page.Action>
<Button variant="outline" onclick={() => window.open('https://classroomio.com/docs/api/reference', '_blank')}>
<Button variant="outline" onclick={() => window.open('https://classroomio.com/docs/api', '_blank')}>
<ArrowUpRightIcon size={16} />
{$t('automation.api.view_docs')}
</Button>
+10 -17
View File
@@ -2,23 +2,16 @@ node_modules
.DS_Store
.cache
.vercel
.output
.nitro
.wrangler
/build/
/api/
/server/build
/public/build
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
.tanstack
.react-router
.dev.vars
src/routeTree.gen.ts
.source
worker-configuration.d.ts
.react-router
# Blume build output, and the packaged assets served by the worker.
/dist/
/build/
/.blume/
/.blume-verify/
/.astro/
# Fetched at build time by scripts/fetch-openapi.mjs.
/openapi/
.blume-verify/
+1 -1
View File
@@ -1 +1 @@
v20.19.3
v22.12.0
+42 -8
View File
@@ -1,14 +1,48 @@
# docs
This is a Tanstack Start application generated with
[Create Fumadocs](https://github.com/fuma-nama/fumadocs).
The ClassroomIO documentation site, built with [Blume](https://useblume.dev) (Astro + Vite). Served at
[classroomio.com/docs](https://classroomio.com/docs).
Run development server:
## Requirements
Node **>= 22.12** (see `.nvmrc`). The rest of the monorepo is still on Node 20, so switch versions
before working here:
```bash
npm run dev
# or
pnpm dev
# or
yarn dev
nvm use
```
## Develop
```bash
pnpm dev # fetches the OpenAPI spec, then starts the dev server
pnpm build # builds to dist/, then packages to build/ for the worker
pnpm preview # builds, then serves through the worker on :4173
pnpm validate # checks internal links and heading anchors
```
Use `pnpm preview` before deploying anything that touches routing. `pnpm dev` serves pages straight
from Blume and never sees Cloudflare's asset server, so it cannot show you the `_redirects` rules, the
404 page, or the trailing-slash handling — only `preview` runs the site the way production does.
## How it's served
The site is fully static. `classroomio.com/docs/*` is proxied by the marketing site
(`apps/website/src/hooks.server.ts`) to the `cio-docs` worker, which serves the build as static
assets — there is no server-side rendering.
Two things follow from being mounted at `/docs`:
- **Links** are authored root-relative (`/course-enrollment`). Blume prepends the base at build time.
- **Images** must be written with the base (`/docs/certificates-rules.webp`). Blume deliberately does
not rebase images, so an unprefixed path would 404.
`scripts/package-assets.mjs` moves `dist/` under `build/docs/` to match the served path and writes the
`_redirects` file (`/docs` → `/docs/home`, and the legacy `/docs/how-to-guides/*` → `/docs/*`).
## API reference
`/docs/api` is the Scalar API reference, rendered at build time (the old `/docs/api/reference` path
301s here). `scripts/fetch-openapi.mjs` pulls the spec from R2 and injects the bearer auth scheme via
`@cio/utils/openapi/public-api`. Because it is static, a new spec reaches the site only on a rebuild —
`.github/workflows/upload-openapi-spec.yml` triggers one automatically.
-16
View File
@@ -1,16 +0,0 @@
@import 'tailwindcss';
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css';
@source "../../../packages/ui/src/**/*.{ts,tsx}";
/*
* The docs layout is nested inside HomeLayout, whose navbar is `sticky h-14 top-0`
* (3.5rem) but does not expose its height to the docs layout. Fumadocs hardcodes
* `--fd-docs-height: 100dvh`, so the sidebar and TOC end up 3.5rem taller than the
* visible area and their bottom (footer / last nav items) is clipped below the fold.
* Subtract the navbar height so both columns can scroll to the end.
*/
#nd-docs-layout {
--fd-docs-height: calc(100dvh - 3.5rem);
}
-15
View File
@@ -1,15 +0,0 @@
import defaultMdxComponents from 'fumadocs-ui/mdx';
import type { MDXComponents } from 'mdx/types';
export function getMDXComponents(components?: MDXComponents) {
return {
...defaultMdxComponents,
...components
} satisfies MDXComponents;
}
export const useMDXComponents = getMDXComponents;
declare global {
type MDXProvidedComponents = ReturnType<typeof getMDXComponents>;
}
-28
View File
@@ -1,28 +0,0 @@
import { Link } from 'react-router';
import { HomeLayout } from 'fumadocs-ui/layouts/home';
export function NotFound() {
return (
<HomeLayout
nav={{
title: 'ClassroomIO'
}}
className="justify-center py-32 text-center"
>
<div className="flex flex-col items-center gap-4">
<h1 className="text-fd-muted-foreground text-6xl font-bold">404</h1>
<h2 className="text-2xl font-semibold">Page Not Found</h2>
<p className="text-fd-muted-foreground max-w-md">
The page you are looking for might have been removed, had its name changed, or is
temporarily unavailable.
</p>
<Link
to="/home"
className="bg-fd-primary text-fd-primary-foreground mt-4 rounded-lg px-4 py-2 text-sm font-medium transition-opacity hover:opacity-90"
>
Back to Home
</Link>
</div>
</HomeLayout>
);
}
-38
View File
@@ -1,38 +0,0 @@
import type { AppLoadContext, EntryContext } from 'react-router';
import { ServerRouter } from 'react-router';
import { isbot } from 'isbot';
import { renderToReadableStream } from 'react-dom/server';
export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
routerContext: EntryContext,
_loadContext: AppLoadContext
) {
let shellRendered = false;
const userAgent = request.headers.get('user-agent');
const body = await renderToReadableStream(
<ServerRouter context={routerContext} url={request.url} />,
{
onError(error: unknown) {
responseStatusCode = 500;
if (shellRendered) {
console.error(error);
}
}
}
);
shellRendered = true;
if ((userAgent && isbot(userAgent)) || routerContext.isSpaMode) {
await body.allReady;
}
responseHeaders.set('Content-Type', 'text/html');
return new Response(body, {
headers: responseHeaders,
status: responseStatusCode
});
}
-68
View File
@@ -1,68 +0,0 @@
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
import { SquareArrowOutUpRight } from 'lucide-react';
export function baseOptions(): BaseLayoutProps {
return {
nav: {
title: (
<div className="flex items-center gap-2">
<img src="/docs/logo-192.png" className="size-8" alt="ClassroomIO logo" />
<span className="font-medium">ClassroomIO</span>
</div>
)
},
links: [
{
text: (
<div className="flex items-center gap-2">
Contact <SquareArrowOutUpRight />
</div>
),
url: 'https://twitter.com/classroomio',
external: true
},
{
text: (
<div className="flex items-center gap-2">
Demo <SquareArrowOutUpRight />
</div>
),
url: 'https://dub.sh/ciodemo',
external: true
},
{
text: (
<div className="flex items-center gap-2">
Feedback <SquareArrowOutUpRight />
</div>
),
url: 'https://feedback.classroomio.com',
external: true
},
{
text: (
<div className="flex items-center gap-2">
Dashboard <SquareArrowOutUpRight />
</div>
),
url: 'https://app.classroomio.com/',
external: true
},
{
type: 'icon',
label: 'Discord',
icon: (
<img
src="/docs/discord-blue.png"
alt="discord icon"
className="w-7 transition-all duration-500 hover:scale-90"
/>
),
text: 'Discord',
url: 'https://dub.sh/ciodiscord',
external: true
}
],
githubUrl: 'https://github.com/classroomio/classroomio'
};
}
-35
View File
@@ -1,35 +0,0 @@
import { loader, multiple } from 'fumadocs-core/source';
import * as icons from 'lucide-static';
import { docs } from 'fumadocs-mdx:collections/server';
export const source = loader({
source: multiple({
docs: docs.toFumadocsSource()
}),
baseUrl: '/',
icon(icon) {
if (!icon) {
return;
}
if (icon in icons) {
return icons[icon as keyof typeof icons];
}
}
});
export async function getDocsRouteData(slugs: string[]) {
const page = source.getPage(slugs);
if (!page) {
return null;
}
if (page.data.type === 'docs') {
return {
tree: source.pageTree as object,
slugs,
pageType: 'docs' as const,
path: page.path
};
}
}
-175
View File
@@ -1,175 +0,0 @@
import * as React from 'react';
import type { LinksFunction, MetaFunction } from 'react-router';
import {
isRouteErrorResponse,
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
useRouteError
} from 'react-router';
import { RootProvider } from 'fumadocs-ui/provider/react-router';
import { NotFound } from '@/components/not-found';
import './app.css';
const DEFAULT_META_TITLE =
'ClassroomIO Docs | Customer, Partner, and Employee Education';
const DEFAULT_META_DESCRIPTION =
'Learn how to create customer academies, partner certification programs, and employee training workflows in ClassroomIO.';
const DEFAULT_OG_IMAGE_URL = 'https://brand.cdn.clsrio.com/og/classroomio-opengraph.jpg';
const DEFAULT_OG_IMAGE_ALT = 'ClassroomIO documentation for customer, partner, and employee education';
export const links: LinksFunction = () => [
{ rel: 'icon', href: '/docs/favicon.ico' }
];
export const meta: MetaFunction = () => [
{
title: DEFAULT_META_TITLE
},
{
name: 'description',
content: DEFAULT_META_DESCRIPTION
},
{
property: 'og:url',
content: 'https://classroomio.com/docs'
},
{
property: 'og:type',
content: 'website'
},
{
property: 'og:title',
content: DEFAULT_META_TITLE
},
{
property: 'og:description',
content: DEFAULT_META_DESCRIPTION
},
{
property: 'og:image',
content: DEFAULT_OG_IMAGE_URL
},
{
property: 'og:image:alt',
content: DEFAULT_OG_IMAGE_ALT
},
{
property: 'og:image:type',
content: 'image/jpeg'
},
{
property: 'og:image:width',
content: '1920'
},
{
property: 'og:image:height',
content: '1080'
},
{
property: 'og:image:secure_url',
content: DEFAULT_OG_IMAGE_URL
},
// WebP fallback for better compression on supported platforms
{
property: 'og:image',
content: 'https://brand.cdn.clsrio.com/og/classroomio-opengraph.webp'
},
{
property: 'og:image:alt',
content: DEFAULT_OG_IMAGE_ALT
},
{
property: 'og:image:type',
content: 'image/webp'
},
{
property: 'og:image:width',
content: '1920'
},
{
property: 'og:image:height',
content: '1080'
},
{
property: 'og:image:secure_url',
content: 'https://brand.cdn.clsrio.com/og/classroomio-opengraph.webp'
},
{
name: 'twitter:card',
content: 'summary_large_image'
},
{
property: 'twitter:domain',
content: 'classroomio.com'
},
{
property: 'twitter:url',
content: 'https://classroomio.com/docs'
},
{
name: 'twitter:title',
content: DEFAULT_META_TITLE
},
{
name: 'twitter:description',
content: DEFAULT_META_DESCRIPTION
},
{
name: 'twitter:creator',
content: '@classroomio'
},
{
name: 'twitter:image',
content: DEFAULT_OG_IMAGE_URL
},
{
name: 'twitter:image:alt',
content: DEFAULT_OG_IMAGE_ALT
}
];
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body className="flex min-h-screen flex-col">
<RootProvider>{children}</RootProvider>
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function App() {
return <Outlet />;
}
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error) && error.status === 404) {
return <NotFound />;
}
return (
<Layout>
<main className="mx-auto flex min-h-screen w-full max-w-3xl flex-col justify-center gap-4 px-6 py-24">
<h1 className="text-3xl font-semibold">Docs Error</h1>
<p className="text-fd-muted-foreground">
The docs app hit an unexpected error while rendering this page.
</p>
</main>
</Layout>
);
}
-8
View File
@@ -1,8 +0,0 @@
import { type RouteConfig, index, route } from '@react-router/dev/routes';
export default [
index('routes/home.tsx'),
route('api/reference', 'routes/api-reference.ts'),
route('api/search', 'routes/search.ts'),
route('*', 'routes/docs.tsx')
] satisfies RouteConfig;
-33
View File
@@ -1,33 +0,0 @@
import { getHtmlDocument } from '@scalar/core/libs/html-rendering';
import {
enrichPublicApiOpenApiSpec,
PUBLIC_API_BEARER_SCHEME,
PUBLIC_API_OPENAPI_URL
} from '@cio/utils/openapi/public-api';
export async function loader() {
const response = await fetch(PUBLIC_API_OPENAPI_URL);
if (!response.ok) {
throw new Response('Failed to load the public API OpenAPI specification.', { status: 502 });
}
const spec = enrichPublicApiOpenApiSpec((await response.json()) as Record<string, unknown>);
return new Response(
getHtmlDocument({
content: JSON.stringify(spec),
pageTitle: 'ClassroomIO Public API Reference',
theme: 'none',
authentication: {
preferredSecurityScheme: PUBLIC_API_BEARER_SCHEME
}
}),
{
headers: {
'content-type': 'text/html; charset=utf-8',
'cache-control': 'public, max-age=300'
}
}
);
}
-129
View File
@@ -1,129 +0,0 @@
import { useMemo } from 'react';
import type * as PageTree from 'fumadocs-core/page-tree';
import browserCollections from 'fumadocs-mdx:collections/browser';
import { isRouteErrorResponse, redirect, useLoaderData, useRouteError } from 'react-router';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
import { HomeLayout } from 'fumadocs-ui/layouts/home';
import { NotFound } from '@/components/not-found';
import { baseOptions } from '@/lib/layout.shared';
const clientLoader = browserCollections.docs.createClientLoader({
component({ toc, frontmatter, default: MDX }) {
return (
<DocsPage toc={toc}>
<DocsTitle>{frontmatter.title}</DocsTitle>
<DocsDescription>{frontmatter.description}</DocsDescription>
<DocsBody>
<MDX />
</DocsBody>
</DocsPage>
);
}
});
export async function loader({ params }: { params: Record<string, string | undefined> }) {
const slug = params['*'];
// How-to guides were flattened to the docs root; permanently redirect the old
// `.../how-to-guides/<page>` URLs to `.../<page>`.
if (slug && slug.includes('how-to-guides/')) {
throw redirect('/' + slug.replace('how-to-guides/', ''), 301);
}
const slugs = slug ? slug.split('/') : ['home'];
const { getDocsRouteData } = await import('@/lib/source.server');
const routeData = await getDocsRouteData(slugs);
if (!routeData) {
throw new Response('Not Found', { status: 404 });
}
if (routeData.pageType === 'docs') {
await clientLoader.preload(routeData.path);
}
return routeData;
}
export default function DocsRoute() {
const data = useLoaderData<typeof loader>();
const tree = useMemo(() => transformPageTree(data.tree as PageTree.Root), [data.tree]);
return (
<HomeLayout {...baseOptions()}>
<DocsLayout
sidebar={{
collapsible: false
}}
themeSwitch={{
enabled: false
}}
searchToggle={{
enabled: false
}}
tree={tree}
>
<MdxContent path={data.path} />
</DocsLayout>
</HomeLayout>
);
}
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error) && error.status === 404) {
return <NotFound />;
}
return (
<HomeLayout {...baseOptions()} className="justify-center py-32 text-center">
<div className="flex flex-col items-center gap-4">
<h1 className="text-2xl font-semibold">Docs Error</h1>
<p className="text-fd-muted-foreground max-w-md">
The docs page could not be rendered.
</p>
</div>
</HomeLayout>
);
}
function MdxContent({ path }: { path: string }) {
const Content = clientLoader.getComponent(path);
return <Content />;
}
function transformPageTree(root: PageTree.Root): PageTree.Root {
function mapNode<T extends PageTree.Node>(item: T): T {
if (typeof item.icon === 'string') {
item = {
...item,
icon: (
<span
dangerouslySetInnerHTML={{
__html: item.icon
}}
/>
)
};
}
if (item.type === 'folder') {
return {
...item,
index: item.index ? mapNode(item.index) : undefined,
children: item.children.map(mapNode)
};
}
return item;
}
return {
...root,
children: root.children.map(mapNode),
fallback: root.fallback ? transformPageTree(root.fallback) : undefined
};
}
-5
View File
@@ -1,5 +0,0 @@
import { redirect } from 'react-router';
export function loader() {
throw redirect('/home');
}
-10
View File
@@ -1,10 +0,0 @@
import { createFromSource } from 'fumadocs-core/search/server';
export async function loader({ request }: { request: Request }) {
const { source } = await import('@/lib/source.server');
const server = createFromSource(source, {
language: 'english'
});
return server.GET(request);
}
+198
View File
@@ -0,0 +1,198 @@
import { defineConfig } from 'blume';
/**
* The site is served at classroomio.com/docs, proxied to this worker by the
* marketing site (apps/website/src/hooks.server.ts). `deployment.base` only
* rewrites URLs in the emitted HTML — it does not nest the build output, so
* scripts/package-assets.mjs moves dist/ under docs/ to match the served path.
*
* Redirects are NOT declared here: under a base, Astro prefixes a redirect's
* source but not its target, so it would emit `url=/guides` instead of
* `/docs/guides`. They live in scripts/package-assets.mjs as a `_redirects` file.
*/
export default defineConfig({
title: 'ClassroomIO Docs | Customer, Partner, and Employee Education',
description:
'Learn how to create customer academies, partner certification programs, and employee training workflows in ClassroomIO.',
logo: {
text: 'ClassroomIO',
image: '/docs/logo-192.png',
href: '/docs'
},
content: {
root: 'content/docs'
},
deployment: {
output: 'static',
site: 'https://classroomio.com',
base: '/docs'
},
github: {
owner: 'classroomio',
repo: 'classroomio',
dir: 'apps/docs'
},
theme: {
mode: 'system'
},
search: {
provider: 'orama'
},
ai: {
llmsTxt: true
},
seo: {
x: {
creator: '@classroomio',
handle: '@classroomio'
}
},
openapi: {
enabled: true,
renderer: 'scalar',
route: '/api',
sources: [{ label: 'API', spec: './openapi/public-api.json' }]
},
navigation: {
tabs: [
{ label: 'Platform', path: '/', icon: 'house' },
{ label: 'Guides', path: '/guides', icon: 'book-open' },
{ label: 'Self Hosting', path: '/self-hosted', icon: 'server' },
{ label: 'API', path: '/api', icon: 'code' }
],
sidebar: [
{
label: 'Platform',
items: [
'/',
{
label: 'Introduction',
items: [
'/what-is-classroomio',
'/terminology',
{
label: 'Quickstart',
items: ['/quickstart/signup', '/quickstart/onboarding']
}
]
},
{
label: 'Core Features',
items: [
'/course',
'/cohorts',
'/mcp',
'/tags',
'/audience',
'/community',
'/organization',
'/student-dashboard',
'/enterprise-sso',
'/token-auth'
]
}
]
},
{
label: 'Guides',
root: '/guides',
items: [
'/guides',
{
label: '', // User Guides
items: [
{
label: 'Get started',
items: ['/guides/create-first-course']
},
{
label: 'Build a course',
items: [
'/guides/course-types',
'/guides/create-exercise',
'/guides/grade-exercise',
'/guides/course-progression',
'/guides/use-math-in-editor',
'/guides/certificates'
]
},
{
label: 'Live classes',
items: ['/guides/live-class', '/guides/take-attendance']
},
{
label: 'Enrollment & students',
items: [
'/guides/course-enrollment',
'/guides/welcome-email',
'/guides/invite-students'
]
},
{
label: 'Publish your academy',
items: [
'/guides/course-landingpage',
'/guides/org-landing-page',
'/guides/academy-sharing-and-branding',
'/guides/custom-domain'
]
},
{
label: 'Organization & team',
items: [
'/guides/customize-organization',
'/guides/invite-team-member',
'/guides/roles-and-permissions'
]
}
]
},
{
label: 'Developer Guides',
items: [
'/guides/contributing',
'/guides/contributing/devs',
'/guides/contributing/design',
'/guides/contributing/cloudflare-setup',
'/guides/contributing/org-site-opengraph-and-favicon',
'/guides/contributing/gitpod',
'/guides/contributing/demo-accounts',
'/guides/contributing/student-dashboard',
'/guides/contributing/adding-translation-to-code'
]
}
]
},
{
label: 'Self Hosting',
root: '/self-hosted',
items: [
'/self-hosted',
{
label: 'Platforms',
items: [
'/self-hosted/docker',
'/self-hosted/digitalocean',
'/self-hosted/railway',
'/self-hosted/coolify',
'/self-hosted/dokploy'
]
},
{
label: 'Configuration',
items: [
'/self-hosted/configuration',
'/self-hosted/configuration/secrets',
'/self-hosted/configuration/email',
'/self-hosted/configuration/storage',
'/self-hosted/configuration/csp',
'/self-hosted/configuration/integrations',
'/self-hosted/configuration/sso',
'/self-hosted/configuration/versions'
]
},
]
}
]
}
});
-94
View File
@@ -1,94 +0,0 @@
---
title: API
description: Use the ClassroomIO public API to manage audience members and courses with organization-scoped API keys.
---
import { Callout } from 'fumadocs-ui/components/callout';
import { Step, Steps } from 'fumadocs-ui/components/steps';
The ClassroomIO public API is available under:
```text
https://api.classroomio.com/public-api/v1
```
Every request must include an organization-scoped API key as a Bearer token.
## Get an API key
Generate keys in the ClassroomIO dashboard as an **organization admin**.
<Steps>
<Step>
Sign in to [ClassroomIO](https://app.classroomio.com).
</Step>
<Step>
In the org sidebar, open the **Automation** section, then **API**.
</Step>
<Step>
Or go directly to [`/org/*/api`](https://app.classroomio.com/org/*/api). The `*` picks your current organization automatically.
</Step>
<Step>
Click **Generate API key**, name the key, and copy the secret immediately. Keys are shown only once and look like `cio_api_...`.
</Step>
<Step>
Send the key on every request as `Authorization: Bearer cio_api_your_key_here`.
</Step>
</Steps>
<Callout type="warn" title="Paid Access">
On ClassroomIO Cloud, API key creation is available on **Early Adopter** and **Enterprise** plans. Self-hosted deployments require **Enterprise**.
</Callout>
## Interactive Reference
- [Open the interactive API reference](/docs/api/reference)
The reference page includes an **Authorize** button. Paste your API key there to try endpoints without writing curl commands.
### Try requests in the reference UI
1. Open the [interactive API reference](/docs/api/reference).
2. Click **Authorize** (lock icon).
3. Select **bearerAuth** and paste your full `cio_api_...` key.
4. Use **Try it** on any endpoint.
### curl example
```bash
curl https://api.classroomio.com/public-api/v1/audience \
-H "Authorization: Bearer cio_api_your_key_here"
```
## Current Surface
Current public API groups:
- `audience`
- `courses`
Implemented route families:
- `GET /public-api/v1/audience`
- `POST /public-api/v1/audience`
- `POST /public-api/v1/audience/assign-courses`
- `GET /public-api/v1/audience/:memberId`
- `PUT /public-api/v1/audience/:memberId`
- `DELETE /public-api/v1/audience/:memberId`
- `GET /public-api/v1/courses`
- `GET /public-api/v1/courses/:courseId`
- `GET /public-api/v1/courses/:courseId/students`
- `GET /public-api/v1/courses/:courseId/export`
## OpenAPI
Raw OpenAPI JSON:
- [Public API OpenAPI JSON](https://api.cdn.clsrio.com/openapi/public-api/openapi-latest.json)
- [Interactive API Reference](/docs/api/reference)
## Notes
- API keys are organization-scoped.
- Enrollment is handled through `POST /public-api/v1/audience/assign-courses`.
- The API documentation will expand as the remaining public API write endpoints are added.
+6 -8
View File
@@ -3,13 +3,11 @@ title: Audience
description: Manage all students in your organization — view status, assign to courses, and handle invites from one place.
---
import { Callout } from 'fumadocs-ui/components/callout';
**Audience** is your org-wide student directory. It shows every person who belongs to your organization — whether they signed up themselves, accepted a course invite, or were added directly. From here you can search, sort, bulk-assign to courses, and manage pending invites.
<Callout type="info" title="Plan limits">
Your plan sets a maximum audience size. When you reach the limit, a banner appears and you cannot invite more students until you upgrade or remove existing members.
</Callout>
:::info[Plan limits]
Your plan sets a maximum audience size. When you reach the limit, a banner appears and you cannot invite more students until you upgrade or remove existing members.
:::
## What Audience shows
@@ -48,6 +46,6 @@ Use **Import Users** to bulk-add students from a CSV file. Use **Export** to dow
## Related guides
- [How Course Enrollment Works](/docs/course-enrollment) — how students join courses
- [How to Invite Students](/docs/invite-students) — send course-specific invites
- [Roles and Permissions](/docs/roles-and-permissions) — difference between students and team members
- [How Course Enrollment Works](/guides/course-enrollment) — how students join courses
- [How to Invite Students](/guides/invite-students) — send course-specific invites
- [Roles and Permissions](/guides/roles-and-permissions) — difference between students and team members
+3 -5
View File
@@ -3,8 +3,6 @@ title: Cohorts
description: Learn what Cohorts are in ClassroomIO, how goals work, and what students see in the Cohorts view.
---
import { Callout } from 'fumadocs-ui/components/callout';
**Cohorts** group multiple courses into a structured learning path. Use them when students need to complete a set of courses toward a shared outcome (onboarding, certification prep, role-based training). Individual courses still live in the LMS; cohorts add goals and a dedicated **Cohorts** view for students.
## Cohorts vs courses
@@ -14,7 +12,7 @@ import { Callout } from 'fumadocs-ui/components/callout';
| **Course** | Single content container with lessons | **Home**, **My Learning**, **Explore** |
| **Cohort** | Multiple courses with shared goals | **Cohorts** in the LMS |
A student can be enrolled in courses without belonging to a cohort, and in a cohort that references several courses. See [Student Dashboard](/docs/student-dashboard) for how those views fit together.
A student can be enrolled in courses without belonging to a cohort, and in a cohort that references several courses. See [Student Dashboard](/student-dashboard) for how those views fit together.
## Admin workflow
@@ -49,6 +47,6 @@ In the LMS, students open **Cohorts** to see cohorts they belong to. Each cohort
- **Courses** — linked courses with progress
- **Goals** — personal goal status when goals are configured
<Callout type="info">
:::info
Legacy deployments may still have data in the original `program` tables. Run the migration script `pnpm --filter @cio/db migrate:programs-to-cohorts` after applying the cohort schema migration to copy existing data into the new tables without dropping the legacy tables.
</Callout>
:::
+5 -7
View File
@@ -3,8 +3,6 @@ title: Community
description: An org-wide Q&A forum where students and instructors can ask questions, share answers, and discuss topics across all courses.
---
import { Callout } from 'fumadocs-ui/components/callout';
**Community** is an org-wide Q&A forum. Anyone in your organization can post a question, answer others, or leave comments. Questions can be tied to a specific course or posted for the whole organization to see.
## How it works
@@ -22,11 +20,11 @@ From the **Community** section in the sidebar, click **Ask Community**. Fill in
Admins can delete any question or comment. Authors can edit or delete their own posts.
<Callout type="info" title="Community vs course Newsfeed">
Community is org-wide and searchable across all courses. The **Newsfeed** tab inside each course is a course-scoped activity feed for announcements and updates — it is not a Q&A forum.
</Callout>
:::info[Community vs course Newsfeed]
Community is org-wide and searchable across all courses. The **Newsfeed** tab inside each course is a course-scoped activity feed for announcements and updates — it is not a Q&A forum.
:::
## Related guides
- [Course](/docs/course) — course-level features including Newsfeed
- [Roles and Permissions](/docs/roles-and-permissions) — who can post and moderate
- [Course](/course) — course-level features including Newsfeed
- [Roles and Permissions](/guides/roles-and-permissions) — who can post and moderate
@@ -1,34 +0,0 @@
---
title: Login with Demo Accounts
description: Quick login guide using demo accounts for testing.
---
import { Callout } from 'fumadocs-ui/components/callout';
This guide will help you get started quickly once you setup our local environment and you want to start using the [dashboard](https://app.classroomio.com).
<Callout>
The assumption of this guide is that you've gone through our README and you've setup `supabase`
and you have `dashboard` running locally.
</Callout>
## Admin Login Credentials
Once you have the `dashboard` service running locally and you have setup supabase, we added seed data that can allow you access an admin account to a demo organization and a student account to a student dashboard. This allows you see what ClassroomIO can do quickly without trying to figure it out.
**Login:** admin@test.com
**Password:** 123456
<div style={{ display: 'flex', justifyContent: 'center', marginY: '16px' }}>
<iframe
width="560"
height="315"
src="https://www.youtube.com/embed/RfE4tQ_BEds?si=uXpYKokMg4SWMkQX"
title="YouTube video player"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
style={{ borderRadius: '8px' }}
></iframe>
</div>
@@ -1,11 +0,0 @@
---
title: Design Contribution Guide
description: Guidelines for contributing design improvements.
---
import { Callout } from 'fumadocs-ui/components/callout';
<Callout type="warn" title="Work in Progress">
This page is a work in progress. Help us expand it by contributing!
</Callout>
@@ -1,11 +0,0 @@
{
"devs": "Developers Guide",
"design": "Design Contribution Guide",
"supabase-cloud": "Supabase Cloud Setup",
"cloudflare-setup": "Cloudflare Setup",
"org-site-opengraph-and-favicon": "Org-Site Open Graph & Favicon",
"gitpod": "Gitpod Setup",
"demo-accounts": "Login with Demo Accounts",
"student-dashboard": "Student Dashboard Guide",
"adding-translation-to-code": "Adding Translations to Code"
}
@@ -1,34 +0,0 @@
---
title: Student Dashboard - Developers Guide
description: Guide to setting up the student dashboard locally.
---
import { Callout } from 'fumadocs-ui/components/callout';
In this guide I will be showing you how to set up a local student dashboard that a student will see. This dashboard will allow students to view their grades, attendance, and other information. This dashboard will also allow students to submit assignments and view their grades for those assignments.
## Follow the steps in this video
### Requirements
<Callout title="ℹ️ Important">
Make sure to use 2 different browser windows: The reason is that one will be for the admin and
another for the students. For the student we will need to set the cookies value to hold the
organization siteName since we are using the subdomain to access the student dashboard like on
production.
</Callout>
### Demo
<div style={{ display: 'flex', justifyContent: 'center', marginY: '16px' }}>
<iframe
width="560"
height="315"
src="https://www.youtube.com/embed/QGTnK39xexs?si=ogFbyl2KK0XaFob4"
title="YouTube video player"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
style={{ borderRadius: '8px' }}
></iframe>
</div>
@@ -1,167 +0,0 @@
---
title: Supabase Cloud Setup
description: Guide to setting up Supabase cloud for ClassroomIO.
---
import { Step, Steps } from 'fumadocs-ui/components/steps';
import { Callout } from 'fumadocs-ui/components/callout';
In this guide, you will learn how to setup a supabase cloud project and link it to your local environment.
## Who is this guide for?
1. You don't want to setup supabase on your local machine
2. You can't run docker on your computer
3. You are experiencing performance issues using supabase locally
4. You want to selfhost our frontend app but use supabase cloud
If your requirement matches any of the above, then this guide is for you.
## Video Walkthrough
<div style={{ display: 'flex', justifyContent: 'center', marginY: '16px' }}>
<iframe
width="560"
height="315"
src="https://www.youtube.com/embed/dAZAZ0oiky0?si=1h-KtppNqJ3U4BTe"
title="YouTube video player"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
style={{ borderRadius: '8px' }}
></iframe>
</div>
## Step by Step Guide
<Steps>
<Step>
### Create a Supabase project
For this step you need to have signed up for supabase and create an account, just visit [supabase.com](https://supabase.com).
Once you have an account just click on the _New Project_ on your dashboard. After that you should see a similar screen to the below, fill in the fields.
![New project in supabase](/new-project.webp)
<Callout title="Important">
Make sure you save your database password somewhere, else you won't be able to get it back
anywhere in the dashboard for security reasons.
</Callout>
</Step>
<Step>
### Setup Supabase CLI
Follow the instructions [here](https://supabase.com/docs/guides/local-development/cli/getting-started) to setup the Supabase CLI on your computer.
</Step>
<Step>
### Login to Supabase via the CLI
Open your terminal and go to the root of the project. Run the following:
```zsh title="Terminal"
supabase login
```
Follow the instruction in your terminal to generate your access token. Paste the token in the console and hit **Enter**
</Step>
<Step>
### Push migrations to your Supabase project
- Go to the supabase dashboard, go to your project, click on the settings icon on the left side bar
![DB URL in the supabase dashboard](/db-url.webp)
- Copy the URL in the image above for your `db url`
- Paste it somewhere
- In the url, replace `[YOUR PASSWORD]` with your db password (which you saved in section 1).
- Then run in your terminal:
```zsh title="Terminal"
supabase db push --db-url "URL HERE"
```
<Callout title="Important">
Make sure you preserve the double quotes before and after the url. Only replace `URL HERE` with
your `db url` that contains your password
</Callout>
#### Result
![Result of supabase db push](/db-push-result.webp)
</Step>
<Step>
### Configure Auth in your Supabase project
Here we will update our email configurations, to prevent supabase from sending confirmation emails on signup. This is because we don't use Supabase for email verification, we handle that internally due to the quality of the email.
![Authentication configuration](/auth-settings.webp)
</Step>
<Step>
### Configure SMTP
Supabase [now requires adding custom SMTP](https://github.com/orgs/supabase/discussions/29370) to your supabase project in order to avoid abuse of their system. Without adding a custom SMTP, your signup requests to your supabase project will fail. To avoid this, you should go to the [SMTP section](https://supabase.com/dashboard/project/_/settings/auth) of your supabase settings.
Make sure to toggle **Enable Custom SMTP** and fill in the following fields:
- Sender email
- Sender name
- SMTP host
- SMTP port
- SMTP username
- SMTP password
![SMTP Settings](/smtp-settings.webp)
<Callout title="ℹ️ Learn More">
Learn more about setting up SMTP [here](https://supabase.com/docs/guides/auth/auth-smtp)
</Callout>
</Step>
<Step>
### Add configurations to your environment variables
Now with all these set up you need to get update the .env file in `apps/dashboard`. You need to update the following environment variables:
```zsh title="env"
PUBLIC_SUPABASE_URL=
PUBLIC_SUPABASE_ANON_KEY=
PRIVATE_SUPABASE_SERVICE_ROLE=
```
To get the correct environment variables, follow the instructions in this image:
![Source of supabase environment variables](/match-env.webp)
</Step>
<Step>
### Tada - That's all
Now you can run:
```zsh title="Terminal"
pnpm dev --filter=dashboard
```
- Open the url in your browser
- Click on Sign up
- And that's it...
<Callout title="ℹ️ Learn More">
Visit the [Sign up guide](/docs/quickstart/signup) to learn how to signup
</Callout>
</Step>
</Steps>
+13 -15
View File
@@ -3,9 +3,7 @@ title: Course
description: Overview of courses in ClassroomIO — lifecycle, settings, admin areas, and links to detailed guides.
---
import { Callout } from 'fumadocs-ui/components/callout';
A **course** in ClassroomIO is a container for lessons, exercises, people, and settings. Students enroll into a course to access its content in the LMS. This page summarizes how courses work for admins. For step-by-step setup, start with [Creating Your First Course](/docs/create-first-course).
A **course** in ClassroomIO is a container for lessons, exercises, people, and settings. Students enroll into a course to access its content in the LMS. This page summarizes how courses work for admins. For step-by-step setup, start with [Creating Your First Course](/guides/create-first-course).
## Course lifecycle
@@ -15,7 +13,7 @@ A **course** in ClassroomIO is a container for lessons, exercises, people, and s
| **Published** | Live on your academy; visible in Explore and on the public landing page |
| **Deleted** | Soft-deleted; no longer appears anywhere in the app |
Publishing makes a course discoverable. Combine publish state with **Allow New Students**, price, and invites to control who can actually join. See [How Course Enrollment Works](/docs/course-enrollment).
Publishing makes a course discoverable. Combine publish state with **Allow New Students**, price, and invites to control who can actually join. See [How Course Enrollment Works](/guides/course-enrollment).
## Key settings
@@ -26,9 +24,9 @@ Publishing makes a course discoverable. Combine publish state with **Allow New S
| **Price** | **Landing page** editor | Free vs paid enrollment |
| **Course type** | Course **Settings** | Changes nav, compliance, and public access behavior |
<Callout type="info" title="Course type changes more than a label">
Self-paced, Live class, Compliance, and Public types change what admins and students see in the app. See [Course Types](/docs/course-types) for when to use each.
</Callout>
:::info[Course type changes more than a label]
Self-paced, Live class, Compliance, and Public types change what admins and students see in the app. See [Course Types](/guides/course-types) for when to use each.
:::
## Admin areas inside a course
@@ -47,18 +45,18 @@ When you open a course from the **Courses** list, the sidebar gives you:
The **People** tab lists everyone in the course group. Members appear when they enroll or when you add them from **Add people**. Email-only invites show as **Pending** until the student signs in.
Enrollment paths (public, invite link, admin assignment, paid) are documented in [How Course Enrollment Works](/docs/course-enrollment). For sending invites, see [How to Invite Students](/docs/invite-students).
Enrollment paths (public, invite link, admin assignment, paid) are documented in [How Course Enrollment Works](/guides/course-enrollment). For sending invites, see [How to Invite Students](/guides/invite-students).
## What students see
After enrolling, students open the course from **Home** or **My Learning** in the [Student Dashboard](/docs/student-dashboard). Published courses they have not joined yet may appear in **Explore**.
After enrolling, students open the course from **Home** or **My Learning** in the [Student Dashboard](/student-dashboard). Published courses they have not joined yet may appear in **Explore**.
## Related guides
- [Creating Your First Course](/docs/create-first-course)
- [Course Types](/docs/course-types)
- [How Course Enrollment Works](/docs/course-enrollment)
- [How to Invite Students](/docs/invite-students)
- [Create a Course Landing Page](/docs/course-landingpage)
- [Student Dashboard](/docs/student-dashboard)
- [Creating Your First Course](/guides/create-first-course)
- [Course Types](/guides/course-types)
- [How Course Enrollment Works](/guides/course-enrollment)
- [How to Invite Students](/guides/invite-students)
- [Create a Course Landing Page](/guides/course-landingpage)
- [Student Dashboard](/student-dashboard)
+16 -19
View File
@@ -3,27 +3,24 @@ title: Enterprise SSO
description: Configure Single Sign-On (SSO) with OIDC for your organization.
---
import { Callout } from 'fumadocs-ui/components/callout';
# Enterprise SSO
ClassroomIO supports Single Sign-On (SSO) via OpenID Connect (OIDC), allowing your organization members to sign in using your existing identity provider (IdP).
<Callout type="info" title="Enterprise Feature">
SSO requires an **Enterprise** plan.
**Cloud:** [Contact us](mailto:support@classroomio.com) to upgrade.
**Self-hosted:** Requires an Enterprise license key. If you are not using Enterprise-only features, you do not need a key.
[Book a call](http://cal.com/classroomio/enterprise) to get your license.
</Callout>
:::info[Enterprise Feature]
SSO requires an **Enterprise** plan.
<Callout title="Where to set `LICENSE_KEY` (self-hosted)">
Set `LICENSE_KEY` in the API service environment.
Local development: `apps/api/.env`.
Docker Compose: repository root `.env`.
If you do not use Enterprise-only features, leave `LICENSE_KEY` unset.
</Callout>
**Cloud:** [Contact us](mailto:support@classroomio.com) to upgrade.
**Self-hosted:** Requires an Enterprise license key. If you are not using Enterprise-only features, you do not need a key.
[Book a call](http://cal.com/classroomio/enterprise) to get your license.
:::
:::note[Where to set `LICENSE_KEY` (self-hosted)]
Set `LICENSE_KEY` in the API service environment.
Local development: `apps/api/.env`.
Docker Compose: repository root `.env`.
If you do not use Enterprise-only features, leave `LICENSE_KEY` unset.
See [Self Hosting → SSO & licensing](/self-hosted/configuration/sso) for per-platform placement.
:::
## Supported Providers
@@ -198,4 +195,4 @@ SAML 2.0 support is planned for a future release. Currently, only OIDC is suppor
- `POST /organization/sso` - Create SSO connection (Admin)
- `PUT /organization/sso/policy` - Update SSO policy (Admin)
See the [API documentation](/docs/api) for full details.
See the [API documentation](/api) for full details.
@@ -3,17 +3,14 @@ title: Sharing Your Academy (Open Graph & Favicon)
description: How favicons, link previews, and social embeds work when you share your academy URL on Discord, Slack, iMessage, and other platforms.
---
import { Callout } from 'fumadocs-ui/components/callout';
import { Step, Steps } from 'fumadocs-ui/components/steps';
When you share your academy URL — for example `https://ciodevs.myclassroomio.com` or your [custom domain](/docs/custom-domain) — chat apps and social networks build a **link preview** from two things:
When you share your academy URL — for example `https://ciodevs.myclassroomio.com` or your [custom domain](/guides/custom-domain) — chat apps and social networks build a **link preview** from two things:
1. **Meta tags** in the page HTML (title, description)
2. An **Open Graph image** (`og:image`) — the large preview thumbnail
ClassroomIO generates both automatically for cloud academies. This guide explains what you control in the dashboard and what to expect when you change branding.
For the public homepage editor, see [Customize Your Academy Landing Page](/docs/org-landing-page). For org name, logo, and theme, see [Customize Your Organization](/docs/customize-organization).
For the public homepage editor, see [Customize Your Academy Landing Page](/guides/org-landing-page). For org name, logo, and theme, see [Customize Your Organization](/guides/customize-organization).
## Favicon (browser tab icon)
@@ -27,7 +24,7 @@ The favicon is the small icon shown in the browser tab when someone opens your p
Expand **Settings** in the sidebar, click **Organization**, then open the **Custom Domain** tab.
![Custom favicon settings](/custom-favicon-setup.webp)
![Custom favicon settings](/docs/custom-favicon-setup.webp)
</Step>
<Step>
@@ -41,9 +38,9 @@ Use the image picker in the **Custom favicon** section and click **Save favicon*
If you have **not** uploaded a custom favicon, ClassroomIO uses your **organization logo** (the same image as in **Settings → Organization**). You do not need to upload the same file twice unless you want a different crop or format for the tab icon.
<Callout type="info" title="Where the favicon appears">
The academy favicon is shown on **public org-site pages** (landing page, course catalog, public course pages). It is not applied to the admin dashboard, LMS, or login screens on `app.classroomio.com`.
</Callout>
:::info[Where the favicon appears]
The academy favicon is shown on **public org-site pages** (landing page, course catalog, public course pages). It is not applied to the admin dashboard, LMS, or login screens on `app.classroomio.com`.
:::
## Link previews (Open Graph)
@@ -75,9 +72,9 @@ The generated card includes:
| **Hero subheading or footer tagline** | Description updates on next page load | Regenerated on paid plans (free plans do not show tagline on the image) |
| **Organization logo** | Unchanged | Regenerated |
<Callout type="warn" title="Previews can lag behind saves">
Text meta tags (title and description) update as soon as someone loads your site. The **thumbnail image** is cached for performance. After a branding change, visit your public academy URL once, then share the link again as a **new message** in Discord or Slack if the old preview still appears.
</Callout>
:::warning[Previews can lag behind saves]
Text meta tags (title and description) update as soon as someone loads your site. The **thumbnail image** is cached for performance. After a branding change, visit your public academy URL once, then share the link again as a **new message** in Discord or Slack if the old preview still appears.
:::
### Refreshing Discord or Slack embeds
@@ -95,8 +92,8 @@ Public **course** pages use the course logo or banner for `og:image` when availa
## Related guides
- [Customize Your Organization](/docs/customize-organization) — name, logo, brand color
- [Customize Your Academy Landing Page](/docs/org-landing-page) — hero heading and subheading (used in title and description)
- [Set Up a Custom Domain](/docs/custom-domain) — share links on your own hostname
- [Customize Your Organization](/guides/customize-organization) — name, logo, brand color
- [Customize Your Academy Landing Page](/guides/org-landing-page) — hero heading and subheading (used in title and description)
- [Set Up a Custom Domain](/guides/custom-domain) — share links on your own hostname
For implementation details (API routes, CDN caching, contributor setup), see [Org-Site Open Graph & Favicon](/docs/contributor-guides/org-site-opengraph-and-favicon).
For implementation details (API routes, CDN caching, contributor setup), see [Org-Site Open Graph & Favicon](/guides/contributing/org-site-opengraph-and-favicon).
@@ -3,20 +3,17 @@ title: How to Create Certificates
description: Configure certificate rules for a course, set completion requirements, and let students download their certificates.
---
import { Callout } from 'fumadocs-ui/components/callout';
import { Step, Steps } from 'fumadocs-ui/components/steps';
Certificates recognize students for completing a course. You configure the rules on a per-course basis — how much of the course must be completed, whether a final exercise must be passed, and when certificates expire. Students can then download their certificate from the **Certificates** tab in the LMS.
<Callout type="warn" title="Paid plan required">
Certificate features require a paid plan. The controls are visible on free plans but disabled.
</Callout>
:::warning[Paid plan required]
Certificate features require a paid plan. The controls are visible on free plans but disabled.
:::
## Configure certificate rules
Open your course in the admin dashboard and go to the **Certificates** tab.
![Certificate rules panel with the download toggle and completion threshold](/certificates-rules.webp)
![Certificate rules panel with the download toggle and completion threshold](/docs/certificates-rules.webp)
<Steps>
<Step>
@@ -55,7 +52,7 @@ Enter text in the email message field. This message is included in the notificat
Set up how the certificate looks before the course goes live, so the first student to qualify gets the finished
version. On the **Certificates** tab, click **Customize design** to open the certificate editor.
![Certificate design editor showing the template picker and live preview](/certificates-design-editor.webp)
![Certificate design editor showing the template picker and live preview](/docs/certificates-design-editor.webp)
<Steps>
<Step>
@@ -70,7 +67,7 @@ Choose a **Template** and an accent color to match your brand. A live preview sh
Under **Signatories**, turn on **Show signatory 1** and fill in **Signatory 1 name** and **Signatory 1 role**, then
use **Signature image** to upload a signature graphic. You can enable a second signatory the same way.
![Signatories section with name, role, and signature image upload](/certificates-signatories.webp)
![Signatories section with name, role, and signature image upload](/docs/certificates-signatories.webp)
</Step>
<Step>
@@ -80,21 +77,21 @@ Set a **Certificate ID format** so every issued certificate carries a unique, ve
</Step>
</Steps>
<Callout type="info">
:::info
Finalize the design and signatures before you publish the course. Certificates are generated from the current design
when a student qualifies, so getting it right up front means no one receives an unfinished certificate.
</Callout>
:::
## What students see
Once a student meets all the configured requirements, their certificate appears in the **Certificates** tab when they open the course in the LMS. They can view and download it from there.
![Student certificate view with download buttons](/certificates-student-download.webp)
![Student certificate view with download buttons](/docs/certificates-student-download.webp)
## Related guides
- [Creating Your First Course](/docs/create-first-course) — course setup overview
- [How to Create an Exercise](/docs/create-exercise) — create the exercise you reference as a final exercise
- [How to Grade an Exercise](/docs/grade-exercise) — grade submissions before certificates are issued
- [Control Course Progression](/docs/course-progression): require completion before the certificate unlocks
- [Student Dashboard](/docs/student-dashboard) — where students access their certificates
- [Creating Your First Course](/guides/create-first-course) — course setup overview
- [How to Create an Exercise](/guides/create-exercise) — create the exercise you reference as a final exercise
- [How to Grade an Exercise](/guides/grade-exercise) — grade submissions before certificates are issued
- [Control Course Progression](/guides/course-progression): require completion before the certificate unlocks
- [Student Dashboard](/student-dashboard) — where students access their certificates
@@ -3,7 +3,14 @@ title: Cloudflare Setup
description: Guide to setting up Cloudflare for ClassroomIO.
---
In this guide, you will learn how to get your Cloudflare environment variables and configure R2 buckets. ClassroomIO supports Cloudflare R2 for video uploads, document uploads, and media (images and video thumbnails). For on-premise or data-residency deployments, you can use [MinIO](/docs/self-hosted/docker#optional--object-storage) instead.
In this guide, you will learn how to get your Cloudflare environment variables and configure R2 buckets. ClassroomIO supports Cloudflare R2 for video uploads, document uploads, and media (images and video thumbnails). For on-premise or data-residency deployments, you can use [MinIO](/self-hosted/configuration/storage) instead.
:::note[`CLOUDFLARE_*` is the legacy R2 config]
The `CLOUDFLARE_*` variables below are a **fallback**, read only when the generic `OBJECT_STORAGE_*`
variables are absent. For new deployments prefer `OBJECT_STORAGE_*` — they cover Cloudflare R2 as well
(point `OBJECT_STORAGE_ENDPOINT` at `https://<account>.r2.cloudflarestorage.com`). See
[Self Hosting → Object storage & uploads](/self-hosted/configuration/storage).
:::
## R2 buckets overview
@@ -45,7 +52,7 @@ Used for: uploading videos (presigned PUT) and playing them back (presigned GET)
"AllowedOrigins": ["http://localhost:5173", "https://your-app-domain.com"],
"AllowedMethods": ["GET", "HEAD", "PUT"],
"AllowedHeaders": ["*"],
"MaxAgeSeconds": 3600
"MaxAgeSeconds": 3600,
}
]
```
@@ -75,7 +82,7 @@ If the dashboard uploads or downloads documents directly to/from R2 via presigne
Used for: serving images and video thumbnails (the dashboard loads these via the public URL from `CLOUDFLARE_IMAGE_BUCKET_DOMAIN`). If that URL is on a different origin than the dashboard, the bucket needs CORS for **GET** and **HEAD**.
Org-site **Open Graph preview images** are also stored in this bucket under `og/{siteName}.png` and served at `{CLOUDFLARE_IMAGE_BUCKET_DOMAIN}/og/{siteName}.png`. See [Org-Site Open Graph & Favicon](/docs/contributor-guides/org-site-opengraph-and-favicon).
Org-site **Open Graph preview images** are also stored in this bucket under `og/{siteName}.png` and served at `{CLOUDFLARE_IMAGE_BUCKET_DOMAIN}/og/{siteName}.png`. See [Org-Site Open Graph & Favicon](/guides/contributing/org-site-opengraph-and-favicon).
1. In **R2** → select the **media** bucket.
2. Open **Settings** → **CORS policy**.
@@ -0,0 +1,21 @@
---
title: Login with Demo Accounts
description: Quick login guide using demo accounts for testing.
---
This guide will help you get started quickly once you setup our local environment and you want to start using the [dashboard](https://app.classroomio.com).
:::note
The assumption of this guide is that you've gone through our README and you've setup `supabase`
and you have `dashboard` running locally.
:::
## Admin Login Credentials
Once you have the `dashboard` service running locally and you have setup supabase, we added seed data that can allow you access an admin account to a demo organization and a student account to a student dashboard. This allows you see what ClassroomIO can do quickly without trying to figure it out.
**Login:** admin@test.com
**Password:** 123456
<YouTube id="RfE4tQ_BEds" />
@@ -0,0 +1,8 @@
---
title: Design Contribution Guide
description: Guidelines for contributing design improvements.
---
:::warning[Work in Progress]
This page is a work in progress. Help us expand it by contributing!
:::
@@ -17,7 +17,7 @@ We invite innovative minds to contribute to the evolution of ClassroomIO. If you
Ready to dive into the code and make a real impact? Here's your path:
1. **Fork the Repository:** Fork our repository or use [Gitpod](/docs/contributor-guides/gitpod)
1. **Fork the Repository:** Fork our repository or use [Gitpod](/guides/contributing/gitpod)
2. **Implementation:** Code it out, test it and apply your changes.
@@ -3,8 +3,6 @@ title: Gitpod Setup
description: Guide to setting up Gitpod for ClassroomIO development.
---
import { Callout } from 'fumadocs-ui/components/callout';
## One Click Setup
- This will open a fully configured workspace in your browser with all the necessary dependencies already installed.
@@ -12,11 +10,11 @@ import { Callout } from 'fumadocs-ui/components/callout';
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/rotimi-best/classroomio)
<Callout title="Note">
Gitpod does not utilize `localhost` for accessing services. Instead, it uses its own URL after
starting all the necessary services. The correct URLs to access the project are automatically
inserted into the `.env` file upon initialization.
</Callout>
:::note[Note]
Gitpod does not utilize `localhost` for accessing services. Instead, it uses its own URL after
starting all the necessary services. The correct URLs to access the project are automatically
inserted into the `.env` file upon initialization.
:::
### Viewing URLs in Terminals
@@ -0,0 +1,54 @@
---
title: Contributor Guides
description: Set up ClassroomIO locally, understand the codebase, and contribute code, design, or translations.
---
ClassroomIO is open source and we welcome contributions. These guides cover getting the project
running on your machine, the conventions we follow, and the infrastructure behind the platform.
If you are here to *use* ClassroomIO rather than work on it, start with
[What is ClassroomIO](/what-is-classroomio) instead.
## Get set up
<CardGroup cols={2}>
<Card title="Developers Guide" href="/guides/contributing/devs" icon="code">
Run the monorepo locally: prerequisites, install, and the apps you can start.
</Card>
<Card title="Gitpod Setup" href="/guides/contributing/gitpod" icon="cloud">
Spin up a ready-made cloud dev environment instead of installing locally.
</Card>
<Card title="Login with Demo Accounts" href="/guides/contributing/demo-accounts" icon="user-check">
Sign in to your local instance with seeded demo users.
</Card>
</CardGroup>
## Contribute
<CardGroup cols={2}>
<Card title="Design Contribution Guide" href="/guides/contributing/design" icon="palette">
How to propose and hand off design work.
</Card>
<Card title="Adding Translations to Code" href="/guides/contributing/adding-translation-to-code" icon="languages">
Wire up new copy so it can be translated.
</Card>
<Card title="Student Dashboard Guide" href="/guides/contributing/student-dashboard" icon="layout-dashboard">
How the student-facing dashboard is put together.
</Card>
</CardGroup>
## Infrastructure
<CardGroup cols={2}>
<Card title="Cloudflare Setup" href="/guides/contributing/cloudflare-setup" icon="cloud-cog">
The Cloudflare services the platform depends on and how they are configured.
</Card>
<Card title="Org-Site Open Graph & Favicon" href="/guides/contributing/org-site-opengraph-and-favicon" icon="image">
How OG images and favicons are generated and cached for public org sites.
</Card>
</CardGroup>
:::note[Looking to self-host instead?]
Running ClassroomIO on your own infrastructure is covered separately in
[Self Hosting](/self-hosted).
:::
@@ -3,11 +3,9 @@ title: Org-Site Open Graph & Favicon
description: Contributor reference for dynamic OG image generation, favicon fallback, CDN caching, and social embed behavior on public org-site routes.
---
import { Callout } from 'fumadocs-ui/components/callout';
This document describes how ClassroomIO generates **Open Graph preview images** and **favicons** for public org-site routes (`*.myclassroomio.com`, BYOD custom domains, and self-hosted single-org mode). It covers caching layers, invalidation, and why crawlers such as Discord need a fast, stable image URL.
For admin-facing behavior, see [Sharing Your Academy (Open Graph & Favicon)](/docs/academy-sharing-and-branding).
For admin-facing behavior, see [Sharing Your Academy (Open Graph & Favicon)](/guides/academy-sharing-and-branding).
## Overview
@@ -174,9 +172,9 @@ Rendered from root `+layout.svelte` via `OrgSiteFavicon` when `data.isOrgSite` i
Regeneration runs on the next OG API request or warm fetch after the etag mismatch.
<Callout type="info" title="No save-hook regen">
There is no background job on organization `PUT`. Regen depends on a page visit (warm) or an external fetch of the OG endpoint.
</Callout>
:::info[No save-hook regen]
There is no background job on organization `PUT`. Regen depends on a page visit (warm) or an external fetch of the OG endpoint.
:::
## Self-hosted notes
@@ -194,6 +192,6 @@ Regeneration runs on the next OG API request or warm fetch after the etag mismat
## Related docs
- [Sharing Your Academy (Open Graph & Favicon)](/docs/academy-sharing-and-branding) — admin-facing guide
- [Cloudflare Setup](/docs/contributor-guides/cloudflare-setup) — R2 media bucket and `CLOUDFLARE_IMAGE_BUCKET_DOMAIN`
- [Customize Your Organization](/docs/customize-organization) — logo, name, theme (user docs)
- [Sharing Your Academy (Open Graph & Favicon)](/guides/academy-sharing-and-branding) — admin-facing guide
- [Cloudflare Setup](/guides/contributing/cloudflare-setup) — R2 media bucket and `CLOUDFLARE_IMAGE_BUCKET_DOMAIN`
- [Customize Your Organization](/guides/customize-organization) — logo, name, theme (user docs)
@@ -0,0 +1,21 @@
---
title: Student Dashboard - Developers Guide
description: Guide to setting up the student dashboard locally.
---
In this guide I will be showing you how to set up a local student dashboard that a student will see. This dashboard will allow students to view their grades, attendance, and other information. This dashboard will also allow students to submit assignments and view their grades for those assignments.
## Follow the steps in this video
### Requirements
:::note[ℹ️ Important]
Make sure to use 2 different browser windows: The reason is that one will be for the admin and
another for the students. For the student we will need to set the cookies value to hold the
organization siteName since we are using the subdomain to access the student dashboard like on
production.
:::
### Demo
<YouTube id="QGTnK39xexs" />
@@ -3,9 +3,6 @@ title: How Course Enrollment Works
description: Understand the ways students can join a course in ClassroomIO—public enrollment, invites, admin assignment, and paid access—and how to restrict who can enroll.
---
import { Callout } from 'fumadocs-ui/components/callout';
import { Step, Steps } from 'fumadocs-ui/components/steps';
Enrollment is how a student becomes a member of a course group and gets access to lessons, exercises, and the LMS. ClassroomIO supports several enrollment paths. Which ones are available depends on your course price, publish status, and settings.
## Enrollment paths at a glance
@@ -40,9 +37,9 @@ On a **free** course (`cost` is 0) with **Allow New Students** enabled, anyone w
No invite token is required. This is the default for free, published courses.
<Callout type="info" title="Allow New Students">
In the course dashboard, go to **Settings** and find **Allow New Students**. When enabled, anyone can self-enroll from the course landing page. When disabled, the public **Enroll** button is hidden and open self-enrollment is blocked — but **course invite links** and **People → Add people** still work.
</Callout>
:::info[Allow New Students]
In the course dashboard, go to **Settings** and find **Allow New Students**. When enabled, anyone can self-enroll from the course landing page. When disabled, the public **Enroll** button is hidden and open self-enrollment is blocked — but **course invite links** and **People → Add people** still work.
:::
## Email invites
@@ -54,11 +51,11 @@ From **People → Add people**, you enter one or more email addresses and Classr
When a student opens the link, they sign in (or sign up) and click **Join course**. ClassroomIO validates the token, expiry, and usage limit before enrolling them.
For step-by-step instructions, see [How to Invite Students](/docs/invite-students).
For step-by-step instructions, see [How to Invite Students](/guides/invite-students).
<Callout type="info" title="Email invites work on closed or unpublished courses">
**Allow New Students** can be off and email invites still work. The toggle only blocks unsolicited self-enrollment from the landing page; it does not block people you explicitly invited. Invites also work on **unpublished** courses, so you can pre-enroll students before a course goes live.
</Callout>
:::info[Email invites work on closed or unpublished courses]
**Allow New Students** can be off and email invites still work. The toggle only blocks unsolicited self-enrollment from the landing page; it does not block people you explicitly invited. Invites also work on **unpublished** courses, so you can pre-enroll students before a course goes live.
:::
## Direct add (People)
@@ -68,9 +65,9 @@ From the course dashboard, open **People → Add people**, select the students y
This does **not** require **Allow New Students** to be on. Use it for closed courses where you control exactly who gets access.
<Callout title="Already enrolled students">
Revoking an invite or turning off public enrollment does not remove students who have already joined. It only blocks future enrollments through that path.
</Callout>
:::note[Already enrolled students]
Revoking an invite or turning off public enrollment does not remove students who have already joined. It only blocks future enrollments through that path.
:::
## Paid courses
@@ -83,7 +80,7 @@ Paid courses are useful when you want to prevent casual sign-ups on a free landi
| Goal | Suggested setup |
| --- | --- |
| Anyone can join | Free course, published, **Allow New Students** on |
| Only people you email invites to | **Allow New Students** off (optional), send [email invites](/docs/invite-students) |
| Only people you email invites to | **Allow New Students** off (optional), send [email invites](/guides/invite-students) |
| Only people you add manually | **Allow New Students** off, use **People → Add people** to directly enroll existing students |
| Paywall + optional invites | Set a price, send invites to buyers who should skip or supplement checkout |
| Visible in catalog, enrollment closed | Published course, **Allow New Students** off, use invites or **People** to add members |
@@ -102,9 +99,9 @@ Restricting who can **join** a course is not the same as hiding it from every st
| **Price** | Whether open self-enrollment requires payment |
| **Internal Enrollment Only** (org-wide) | When on, any student in your organization can self-enroll in any published course, bypassing per-course **Allow New Students** |
<Callout type="warn" title="Explore shows published courses">
**Explore** in the student LMS lists published courses the student has not enrolled in yet. Turning off **Allow New Students** blocks open enrollment from the landing page. It does **not** remove the course from Explore. ClassroomIO does not support hiding a published course from specific people in Explore today.
</Callout>
:::warning[Explore shows published courses]
**Explore** in the student LMS lists published courses the student has not enrolled in yet. Turning off **Allow New Students** blocks open enrollment from the landing page. It does **not** remove the course from Explore. ClassroomIO does not support hiding a published course from specific people in Explore today.
:::
**Workarounds:**
@@ -112,7 +109,7 @@ Restricting who can **join** a course is not the same as hiding it from every st
- Use **Allow New Students** off plus invites or **People** so only the people you choose can actually join.
- Accept that enrolled students may see the course in Explore even when they cannot self-enroll.
For where students see enrolled vs available courses, see [Student Dashboard](/docs/student-dashboard).
For where students see enrolled vs available courses, see [Student Dashboard](/student-dashboard).
## People and membership
@@ -125,9 +122,9 @@ When a student enrolls or you add them from **People**, they become a member of
The **People** tab in the course dashboard is the source of truth for course membership.
<Callout type="info" title="People vs Compliance status">
**Compliance** courses also track completion status (for example **not started**). That is separate from membership. A student can appear in **People** as soon as they are enrolled while still showing **not started** on the Compliance dashboard until they begin the course.
</Callout>
:::info[People vs Compliance status]
**Compliance** courses also track completion status (for example **not started**). That is separate from membership. A student can appear in **People** as soon as they are enrolled while still showing **not started** on the Compliance dashboard until they begin the course.
:::
## Organization-level access settings (separate from course enrollment)
@@ -181,10 +178,10 @@ Enable **Disable Signup** and **Internal Enrollment Only** in **Settings → Aut
## Related guides
- [How to Invite Students](/docs/invite-students) — create, send, and revoke course invite links
- [Customize the Welcome Email](/docs/welcome-email): the message students get when they enroll
- [Roles and Permissions](/docs/roles-and-permissions) — who can create courses and access the admin dashboard
- [Student Dashboard](/docs/student-dashboard) — Home, My Learning, Explore, and Cohorts
- [Set Up a Custom Domain](/docs/custom-domain) — org URLs used in enrollment links
- [Create a Course Landing Page](/docs/course-landingpage) — pricing, publish state, and what prospects see before enrolling
- [Creating Your First Course](/docs/create-first-course) — create and publish a course
- [How to Invite Students](/guides/invite-students) — create, send, and revoke course invite links
- [Customize the Welcome Email](/guides/welcome-email): the message students get when they enroll
- [Roles and Permissions](/guides/roles-and-permissions) — who can create courses and access the admin dashboard
- [Student Dashboard](/student-dashboard) — Home, My Learning, Explore, and Cohorts
- [Set Up a Custom Domain](/guides/custom-domain) — org URLs used in enrollment links
- [Create a Course Landing Page](/guides/course-landingpage) — pricing, publish state, and what prospects see before enrolling
- [Creating Your First Course](/guides/create-first-course) — create and publish a course
@@ -3,10 +3,7 @@ title: Create a Course Landing Page
description: Build and edit a course landing page, including pricing, hero buttons, preview, and publish settings.
---
import { Callout } from 'fumadocs-ui/components/callout';
import { Step, Steps } from 'fumadocs-ui/components/steps';
A **course landing page** is the public page for one course. Prospects read the description, see pricing, and click **Enroll** if enrollment is open. This is separate from your [academy landing page](/docs/org-landing-page), which is the academy homepage.
A **course landing page** is the public page for one course. Prospects read the description, see pricing, and click **Enroll** if enrollment is open. This is separate from your [academy landing page](/guides/org-landing-page), which is the academy homepage.
## Open the editor
@@ -47,7 +44,7 @@ Use the preview control next to **Save** to see the page. Confirm behavior on th
| **Instructors** | Instructor cards |
| **Footer** | Supporting links and copy |
Hero and pricing interact with [course enrollment](/docs/course-enrollment): a free course with **Allow New Students** on shows an open **Enroll** button; paid or closed courses route visitors through payment or invites.
Hero and pricing interact with [course enrollment](/guides/course-enrollment): a free course with **Allow New Students** on shows an open **Enroll** button; paid or closed courses route visitors through payment or invites.
## Pricing and currency
@@ -58,9 +55,9 @@ Set the course price in the **Pricing** section. ClassroomIO supports these curr
| US Dollar | USD |
| Nigerian Naira | NGN |
<Callout type="info" title="Other currencies">
If you need a currency outside USD and NGN, that requires product support today. Pick the closest supported currency or keep the course free and handle payment outside ClassroomIO.
</Callout>
:::info[Other currencies]
If you need a currency outside USD and NGN, that requires product support today. Pick the closest supported currency or keep the course free and handle payment outside ClassroomIO.
:::
## Publish and Allow New Students
@@ -72,24 +69,24 @@ Landing page content can be ready before you open enrollment:
| **Allow New Students** | Public **Enroll** works (for free courses) |
| **Price > 0** | Enrollment requires payment or a valid invite |
See [How Course Enrollment Works](/docs/course-enrollment) for closed cohorts and invite-only access.
See [How Course Enrollment Works](/guides/course-enrollment) for closed cohorts and invite-only access.
## Preview vs live site
The in-app preview shows layout and copy. Test **Enroll**, external links, and payment on your live academy URL (subdomain or [custom domain](/docs/custom-domain)) before sharing publicly.
The in-app preview shows layout and copy. Test **Enroll**, external links, and payment on your live academy URL (subdomain or [custom domain](/guides/custom-domain)) before sharing publicly.
<Callout type="warn" title="Org landing page preview">
The org homepage preview in **Settings → Landing Page** disables link clicks. Course landing previews should still be checked on the live site for enrollment flows.
</Callout>
:::warning[Org landing page preview]
The org homepage preview in **Settings → Landing Page** disables link clicks. Course landing previews should still be checked on the live site for enrollment flows.
:::
## What students see after enrolling
After a successful enroll, students land in the [Student Dashboard](/docs/student-dashboard) (**Home** / **My Learning**). The course may disappear from **Explore** once they are a member.
After a successful enroll, students land in the [Student Dashboard](/student-dashboard) (**Home** / **My Learning**). The course may disappear from **Explore** once they are a member.
## Related guides
- [How Course Enrollment Works](/docs/course-enrollment) — who can join and how Explore relates to publish settings
- [How to Invite Students](/docs/invite-students) — invite links when public enroll is off
- [Course Types](/docs/course-types) — how type affects public vs LMS access
- [Set Up a Custom Domain](/docs/custom-domain) — host for your landing page URL
- [Creating Your First Course](/docs/create-first-course)
- [How Course Enrollment Works](/guides/course-enrollment) — who can join and how Explore relates to publish settings
- [How to Invite Students](/guides/invite-students) — invite links when public enroll is off
- [Course Types](/guides/course-types) — how type affects public vs LMS access
- [Set Up a Custom Domain](/guides/custom-domain) — host for your landing page URL
- [Creating Your First Course](/guides/create-first-course)
@@ -3,9 +3,6 @@ title: Control Course Progression
description: Require students to move through a course in order, with lessons and exercises that only complete when your rules are met.
---
import { Callout } from 'fumadocs-ui/components/callout';
import { Step, Steps } from 'fumadocs-ui/components/steps';
By default students can open any unlocked item in any order. With **sequential** progression they must finish each item
before the next one unlocks, and you decide what "finished" means for every lesson and exercise. This is the backbone of
a structured training flow: watch the video, pass the quiz, earn the certificate.
@@ -19,12 +16,12 @@ In the course's **Settings** tab, find the **Student progression** section and p
| **Free** | Students open any unlocked item in any order and mark lessons complete themselves. |
| **Sequential** | The next item stays locked until the current (or previous) item is complete. |
![Student progression setting with Free and Sequential options](/progression-mode.webp)
![Student progression setting with Free and Sequential options](/docs/progression-mode.webp)
<Callout type="info">
:::info
If a course is just a video and a quiz, turn off **Content grouping** in the same settings so the content list is a
flat list instead of grouped sections.
</Callout>
:::
## Set how a lesson completes
@@ -36,12 +33,12 @@ Open a lesson, go to its **Settings** tab, and choose a **Completion rule**:
| **Video watch** | The student watches the selected videos. Set the **Watch threshold (%)** (for example 95%). |
| **None** | The lesson never blocks progression. |
![Lesson Completion rule options with Manual, Video watch, and None](/progression-completion-rule.webp)
![Lesson Completion rule options with Manual, Video watch, and None](/docs/progression-completion-rule.webp)
<Callout type="warn">
:::warning
**Video watch** only enforces uploaded videos. While the rule is active the player blocks skipping ahead and pauses when
the student switches tabs, so the watch percentage reflects real viewing. The lesson completes once the threshold is reached.
</Callout>
:::
## Set how an exercise completes
@@ -58,7 +55,7 @@ In the course's **Certificates** settings, turn on certificate download and set
100%). The certificate unlocks only after the student completes that share of the course, which, under sequential
progression, means they have watched the videos and finished the exercises in order.
See [How to Create Certificates](/docs/certificates) for templates, signatures, and the certificate ID.
See [How to Create Certificates](/guides/certificates) for templates, signatures, and the certificate ID.
## What students see
@@ -72,7 +69,7 @@ quiz that follows a lesson stays locked until that lesson is complete. In the co
out with a lock icon and cannot be opened; it unlocks automatically once the previous required item is done. If a student
opens a not-yet-available item directly, they see a **Complete previous content first** notice.
![Locked exercise in the content list with a lock icon](/progression-locked.webp)
![Locked exercise in the content list with a lock icon](/docs/progression-locked.webp)
</Step>
<Step>
@@ -81,7 +78,7 @@ opens a not-yet-available item directly, they see a **Complete previous content
On a **Video watch** lesson, the progress percentage climbs as they watch. Trying to jump ahead shows a "you cannot skip
ahead" notice, and the video pauses if they leave the tab.
![Video player showing the "You cannot skip ahead on this video" notice](/progression-cant-skip.webp)
![Video player showing the "You cannot skip ahead on this video" notice](/docs/progression-cant-skip.webp)
</Step>
<Step>
@@ -90,14 +87,14 @@ ahead" notice, and the video pauses if they leave the tab.
Reaching the watch threshold marks the lesson complete and unlocks the next item. After they finish the final
requirement, the course is complete and they can view and download their certificate.
![Course requirements complete modal showing congratulations and View certificate button](/progression-student-course-complete.webp)
![Course requirements complete modal showing congratulations and View certificate button](/docs/progression-student-course-complete.webp)
</Step>
</Steps>
## Related guides
- [Course Types](/docs/course-types): Self-paced, Live class, Compliance, and Public
- [How to Create an Exercise](/docs/create-exercise): build the quiz that gates progression
- [How to Create Certificates](/docs/certificates): what students earn at the end
- [Run a Live Class](/docs/live-class): schedule live sessions alongside on-demand content
- [Course Types](/guides/course-types): Self-paced, Live class, Compliance, and Public
- [How to Create an Exercise](/guides/create-exercise): build the quiz that gates progression
- [How to Create Certificates](/guides/certificates): what students earn at the end
- [Run a Live Class](/guides/live-class): schedule live sessions alongside on-demand content
@@ -3,8 +3,6 @@ title: Course Types
description: Learn what Self-paced, Live class, Compliance, and Public course types do in ClassroomIO and when to use each.
---
import { Callout } from 'fumadocs-ui/components/callout';
Course **type** is set in each course's **Settings**. It is easy to miss because the dropdown alone does not show every change. Types mainly affect admin navigation, compliance tracking, grading, and whether the course uses the normal enroll-and-LMS flow.
## Types at a glance
@@ -27,7 +25,7 @@ Use self-paced for standard training, onboarding content, and evergreen material
**Live class** adds instructor grading workflows. Admins and tutors see a **Marks** section in the course sidebar to review and grade student work.
Use live class when an instructor scores assignments and you need a dedicated grading view. To schedule sessions with a
join link, automatic reminders, and a calendar invite, see [Run a Live Class](/docs/live-class).
join link, automatic reminders, and a calendar invite, see [Run a Live Class](/guides/live-class).
## Compliance
@@ -41,9 +39,9 @@ join link, automatic reminders, and a calendar invite, see [Run a Live Class](/d
Use compliance for mandatory training where you must track who completed requirements by a deadline.
<Callout type="info" title="Compliance status vs People membership">
A student can appear in **People** as soon as they enroll while still showing **not started** on the Compliance dashboard. Membership and completion status are separate.
</Callout>
:::info[Compliance status vs People membership]
A student can appear in **People** as soon as they enroll while still showing **not started** on the Compliance dashboard. Membership and completion status are separate.
:::
## Public
@@ -51,9 +49,9 @@ Use compliance for mandatory training where you must track who completed require
Use public when you want open educational content without requiring accounts. Public courses are not a substitute for private cohort enrollment.
<Callout type="warn" title="Changing type on an existing course">
Switching type after launch can change which sidebar items appear and how completion is tracked. Pick the type that matches your delivery model before students enroll when possible.
</Callout>
:::warning[Changing type on an existing course]
Switching type after launch can change which sidebar items appear and how completion is tracked. Pick the type that matches your delivery model before students enroll when possible.
:::
## Why types can feel identical in Settings
@@ -68,9 +66,9 @@ Open the course sidebar after changing type to confirm the right tools are avail
## Related guides
- [Course](/docs/course) — course overview and admin areas
- [Run a Live Class](/docs/live-class): schedule sessions, reminders, and calendar invites
- [Control Course Progression](/docs/course-progression): sequential learning and completion rules
- [How Course Enrollment Works](/docs/course-enrollment) — enrollment (not used for Public in the same way)
- [Student Dashboard](/docs/student-dashboard) — student LMS views
- [Creating Your First Course](/docs/create-first-course)
- [Course](/course) — course overview and admin areas
- [Run a Live Class](/guides/live-class): schedule sessions, reminders, and calendar invites
- [Control Course Progression](/guides/course-progression): sequential learning and completion rules
- [How Course Enrollment Works](/guides/course-enrollment) — enrollment (not used for Public in the same way)
- [Student Dashboard](/student-dashboard) — student LMS views
- [Creating Your First Course](/guides/create-first-course)
@@ -3,10 +3,7 @@ title: How to Create an Exercise
description: Learn how to create exercises in ClassroomIO.
---
import { Callout } from 'fumadocs-ui/components/callout';
import { Step, Steps } from 'fumadocs-ui/components/steps';
In ClassroomIO, exercises are a great way to assess students' understanding through practice activities. Once you've [set up your organization](/docs/customize-organization), [created a course](/docs/create-first-course), and [added a lesson](/docs/create-first-course#adding-lessons), you can start creating exercises. Follow the process below to create your first exercise.
In ClassroomIO, exercises are a great way to assess students' understanding through practice activities. Once you've [set up your organization](/guides/customize-organization), [created a course](/guides/create-first-course), and [added a lesson](/guides/create-first-course#adding-lessons), you can start creating exercises. Follow the process below to create your first exercise.
<Steps>
<Step>
@@ -14,7 +11,7 @@ In ClassroomIO, exercises are a great way to assess students' understanding thro
Access the courses page from the dashboard by clicking on **Courses** via the sidebar menu.
![Organization dashboard displaying a sidebar menu](/organization-dashboard.webp)
![Organization dashboard displaying a sidebar menu](/docs/organization-dashboard.webp)
</Step>
@@ -23,7 +20,7 @@ Access the courses page from the dashboard by clicking on **Courses** via the si
On the courses page, click on a course to go to the course page.
![Courses page for organization displaying a sidebar menu and a course card component](/courses-page.webp)
![Courses page for organization displaying a sidebar menu and a course card component](/docs/courses-page.webp)
</Step>
@@ -34,7 +31,7 @@ On the course page, click on **Lessons** via the left sidebar menu and select th
> Alternatively, you can click on the **⌄** icon next to the **Lessons** option to select a lesson.
![Course page displaying sidebar menu with the lesson page open](/course-page.webp)
![Course page displaying sidebar menu with the lesson page open](/docs/course-page.webp)
</Step>
@@ -43,23 +40,23 @@ On the course page, click on **Lessons** via the left sidebar menu and select th
Locate the tabs at the top of the page and select the **Exercises** tab. Then click on the **Add** button. On the modal, select a method to create an exercise. You can create an exercise:
![Course page displaying sidebar menu with the exercise tab open](/exercise.webp)
![Course page displaying sidebar menu with the exercise tab open](/docs/exercise.webp)
1. **From scratch**: Create an exercise by adding the questions yourself. This is handy if you've prepared your questions.
- Enter the exercise name and click on the **Finish** button.
- Click on the newly created exercise and provide the exercise description, due date, and question details.
![A pop-up displaying new exercise with an empty field](/from-scratch.webp)
![A pop-up displaying new exercise with an empty field](/docs/from-scratch.webp)
2. **Using a template**: You can select from an array of templates with predefined exercises complete with questions and options.
- Select a template and click on the **Finish** button.
![A pop-up displaying new exercise with an array of exercise templates](/using-a-template.webp)
![A pop-up displaying new exercise with an array of exercise templates](/docs/using-a-template.webp)
3. **Using AI**: AI helps you generate exercises directly from lesson notes.
- Enter the number of questions and options. Next, click on the **Generate** button.
![A pop-up displaying new exercise with two questions about the exercise content](/using-AI.webp)
![A pop-up displaying new exercise with two questions about the exercise content](/docs/using-AI.webp)
</Step>
@@ -68,7 +65,7 @@ Locate the tabs at the top of the page and select the **Exercises** tab. Then cl
To preview the exercise, click on the **preview icon**. This is to ensure the exercise looks and functions as expected.
![Exercise content edit page](/preview.webp)
![Exercise content edit page](/docs/preview.webp)
</Step>
@@ -77,7 +74,7 @@ To preview the exercise, click on the **preview icon**. This is to ensure the ex
To add a question, click on the **add icon**.
![Exercise content edit page with a red box highlight on the add icon](/add-question.webp)
![Exercise content edit page with a red box highlight on the add icon](/docs/add-question.webp)
</Step>
@@ -86,7 +83,7 @@ To add a question, click on the **add icon**.
To reorder questions, click on the **three dots** > **Reorder Questions**, then drag and drop the questions.
![A pop-up showing the question order for exercise](/reorder-questions.webp)
![A pop-up showing the question order for exercise](/docs/reorder-questions.webp)
</Step>
@@ -102,7 +99,7 @@ After previewing, adding, and rearranging questions, ensure to save your changes
You've successfully created your first exercise and it is now accessible to students enrolled in the course. Move on to the next section to start grading your exercise.
![Course page displaying sidebar menu with the exercise tab open showing the newly created exercise](/new-exercise.webp)
![Course page displaying sidebar menu with the exercise tab open showing the newly created exercise](/docs/new-exercise.webp)
</Step>
</Steps>
@@ -112,18 +109,18 @@ You've successfully created your first exercise and it is now accessible to stud
When a quiz is ready, you can email every enrolled student a one-click link to take it. On the exercise page, open the
**⋯** menu at the top and choose **Notify students**.
![Exercise menu showing the Notify students option](/notify-students-menu.webp)
![Exercise menu showing the Notify students option](/docs/notify-students-menu.webp)
The send runs in the background. A progress indicator appears while emails go out, then confirms when it is done. You can
run it again at any time to nudge students who have not taken the quiz yet.
<Callout type="info">
:::info
Notifying students is handy after a live session: schedule the quiz, then send the notification so attendees know to
complete it. See [Run a Live Class](/docs/live-class).
</Callout>
complete it. See [Run a Live Class](/guides/live-class).
:::
## Related guides
- [How to Grade an Exercise](/docs/grade-exercise): review and score submissions
- [Control Course Progression](/docs/course-progression): require a passing score before the next item unlocks
- [How to Create Certificates](/docs/certificates): issue certificates on completion
- [How to Grade an Exercise](/guides/grade-exercise): review and score submissions
- [Control Course Progression](/guides/course-progression): require a passing score before the next item unlocks
- [How to Create Certificates](/guides/certificates): issue certificates on completion
@@ -3,24 +3,9 @@ title: Creating Your First Course
description: Learn how to create your first course on ClassroomIO.
---
import { Step, Steps } from 'fumadocs-ui/components/steps';
import { Callout } from 'fumadocs-ui/components/callout';
Below is a video guiding you through the process of creating your inaugural course. Should you have any inquiries, feel free to drop a comment in the YouTube comment section, reach out to us on **[Discord](https://classroomio.com/discord)**, or send a DM on **[Twitter](https://x.com/rotimi_best?s=20)**.
Below is a video guiding you through the process of creating your inaugural course. Should you have any inquiries, feel free to drop a comment in the YouTube comment section, reach out to us on **[Discord](https://discord.gg/BSaEzcfu)**, or send a DM on **[Twitter](https://x.com/rotimi_best?s=20)**.
<div style={{ display: 'flex', justifyContent: 'center', marginY: '16px' }}>
<iframe
width="560"
height="315"
src="https://www.youtube.com/embed/ztiZ6vo2gg0?si=v_UUL1YUPyDQ4_pp"
title="YouTube video player"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
style={{ borderRadius: '8px' }}
></iframe>
</div>
<YouTube id="ztiZ6vo2gg0" />
## Procedure
@@ -63,9 +48,9 @@ On the course page, click "Lessons" in the sidebar to access the lessons page. C
Provide the lesson title, select a tutor, date, and optionally lock the lesson or notes.
<Callout title="💡 Tip">
Locking the lesson renders it inaccessible to students upon course publication.
</Callout>
:::note[💡 Tip]
Locking the lesson renders it inaccessible to students upon course publication.
:::
Save your changes to be redirected to the lesson page for material addition.
@@ -95,9 +80,9 @@ To embed slides, navigate to the slides tab and input the link to the slides. Cl
Embedding videos is simple: select the "Videos" tab to open the modal for adding videos. From the "Add By" options in the modal, select the "YouTube Link" tab. Input the link to the YouTube video and click the "Add Video" button. Once added, the video is automatically included in your materials. You can delete the video using the trash icon.
<Callout type="warn">
Please note that for now, we do not support adding videos from local sources.
</Callout>
:::warning
Please note that for now, we do not support adding videos from local sources.
:::
## Publishing a course
@@ -3,9 +3,6 @@ title: Set Up a Custom Domain
description: Connect and verify a custom domain for your ClassroomIO academy and learn which links use your org URL.
---
import { Callout } from 'fumadocs-ui/components/callout';
import { Step, Steps } from 'fumadocs-ui/components/steps';
Every organization gets a default site at `https://<siteName>.myclassroomio.com`. You can connect your own domain (for example `https://learn.yourcompany.com`) so students see your brand in the browser bar and in links.
## How ClassroomIO picks a base URL
@@ -48,9 +45,9 @@ Open your custom domain in a browser. Sign in, open a course landing page, and c
</Step>
</Steps>
<Callout type="warn" title="Verification required">
An unverified custom domain is not used for generated links. Until verification succeeds, ClassroomIO keeps using `<siteName>.myclassroomio.com`.
</Callout>
:::warning[Verification required]
An unverified custom domain is not used for generated links. Until verification succeeds, ClassroomIO keeps using `<siteName>.myclassroomio.com`.
:::
## Which links use your org URL
@@ -61,7 +58,7 @@ Open your custom domain in a browser. Sign in, open a course landing page, and c
| Org site landing page and course catalog | Yes |
| Organization team invite (some flows) | May still use platform URL until fully aligned |
Course invites are documented in [How to Invite Students](/docs/invite-students). Those links should point at your academy, not `app.classroomio.com`, when your domain is verified.
Course invites are documented in [How to Invite Students](/guides/invite-students). Those links should point at your academy, not `app.classroomio.com`, when your domain is verified.
## Email sender address
@@ -73,8 +70,8 @@ Students should sign in on your academy so sessions and redirects stay on your a
## Related guides
- [Customize Your Organization](/docs/customize-organization) — org profile and default subdomain
- [Customize Your Academy Landing Page](/docs/org-landing-page) — public homepage on your domain
- [How to Invite Students](/docs/invite-students) — enrollment links on your academy
- [How Course Enrollment Works](/docs/course-enrollment) — enrollment paths and org signup
- [Organization](/docs/organization) — org settings overview
- [Customize Your Organization](/guides/customize-organization) — org profile and default subdomain
- [Customize Your Academy Landing Page](/guides/org-landing-page) — public homepage on your domain
- [How to Invite Students](/guides/invite-students) — enrollment links on your academy
- [How Course Enrollment Works](/guides/course-enrollment) — enrollment paths and org signup
- [Organization](/organization) — org settings overview
@@ -3,13 +3,9 @@ title: Customize Your Organization
description: Learn how to customize your organization settings.
---
import { Step, Steps } from 'fumadocs-ui/components/steps';
import { Callout } from 'fumadocs-ui/components/callout';
ClassroomIO empowers every organization to tailor their experience to their unique needs. In this guide, you will learn how to customize your organization settings. To access this, from your admin dashboard, navigate to **Settings** > **Organization**.
![ClassroomIO Organization settings page](/customize-organization.webp)
![ClassroomIO Organization settings page](/docs/customize-organization.webp)
## Update Organization Profile
@@ -66,7 +62,7 @@ Upon onboarding, each organization in ClassroomIO is assigned a custom Domain UR
Click the Edit Domain button to navigate to the domain setup page.
![Custom Domain setup](/domain-setup-one.webp)
![Custom Domain setup](/docs/domain-setup-one.webp)
</Step>
@@ -75,7 +71,7 @@ Click the Edit Domain button to navigate to the domain setup page.
Fill the field with your desired URL.
![Edit Custom domain URL](/domain-setup-two.webp)
![Edit Custom domain URL](/docs/domain-setup-two.webp)
</Step>
@@ -86,16 +82,16 @@ Click on Update Domain to save the changes.
> You can click on **View Site** to be redirected to your Organization website with the updated URL.
![Edit Custom domain URL](/domain-setup-three.webp)
![Edit Custom domain URL](/docs/domain-setup-three.webp)
</Step>
</Steps>
For full custom domain setup (DNS, verification, which links use your domain), see [Set Up a Custom Domain](/docs/custom-domain).
For full custom domain setup (DNS, verification, which links use your domain), see [Set Up a Custom Domain](/guides/custom-domain).
For the public homepage (hero, nav, footer), see [Customize Your Academy Landing Page](/docs/org-landing-page).
For the public homepage (hero, nav, footer), see [Customize Your Academy Landing Page](/guides/org-landing-page).
For favicon and link-preview behavior when sharing your academy URL, see [Sharing Your Academy (Open Graph & Favicon)](/docs/academy-sharing-and-branding).
For favicon and link-preview behavior when sharing your academy URL, see [Sharing Your Academy (Open Graph & Favicon)](/guides/academy-sharing-and-branding).
## Set Brand Color
@@ -103,8 +99,8 @@ We understand that every organization has a preferred color that suits their bra
## Related guides
- [Organization](/docs/organization) — org settings overview
- [Customize Your Academy Landing Page](/docs/org-landing-page) — public homepage
- [Set Up a Custom Domain](/docs/custom-domain) — your own hostname
- [Sharing Your Academy (Open Graph & Favicon)](/docs/academy-sharing-and-branding) — link previews and browser tab icon
- [Roles and Permissions](/docs/roles-and-permissions) — team access
- [Organization](/organization) — org settings overview
- [Customize Your Academy Landing Page](/guides/org-landing-page) — public homepage
- [Set Up a Custom Domain](/guides/custom-domain) — your own hostname
- [Sharing Your Academy (Open Graph & Favicon)](/guides/academy-sharing-and-branding) — link previews and browser tab icon
- [Roles and Permissions](/guides/roles-and-permissions) — team access
@@ -3,9 +3,6 @@ title: How to Grade an Exercise
description: Review student submissions, move them through the grading workflow, score answers, and leave feedback.
---
import { Callout } from 'fumadocs-ui/components/callout';
import { Step, Steps } from 'fumadocs-ui/components/steps';
When students submit exercises, their work appears in the **Submissions** tab of the course dashboard. Exercises move through a three-stage workflow: **Submitted → In Progress → Graded**.
## Grading workflow
@@ -18,9 +15,9 @@ When students submit exercises, their work appears in the **Submissions** tab of
Submissions are displayed as cards on a board. Drag a card between columns or open it to change its status manually.
<Callout type="info" title="Auto-graded exercises">
Exercises that use only objective question types (multiple choice, etc.) are auto-graded when submitted. They move directly to **Graded** without manual review. Exercises with open-ended or video questions require manual review and are flagged **Manual grading required**.
</Callout>
:::info[Auto-graded exercises]
Exercises that use only objective question types (multiple choice, etc.) are auto-graded when submitted. They move directly to **Graded** without manual review. Exercises with open-ended or video questions require manual review and are flagged **Manual grading required**.
:::
## Grade a submission
@@ -60,12 +57,12 @@ Click **Submit Grades**. The card moves to **Graded** and the student's score is
Click **Grade with AI** inside the grading modal to get an AI-suggested score and rationale for open-ended answers. Review the suggestion and click **Accept** to apply it or **Reject** to discard it and grade manually.
<Callout type="warn" title="Review AI grades before submitting">
AI grading is a suggestion. Always review the reasoning before accepting, especially for nuanced or context-dependent answers.
</Callout>
:::warning[Review AI grades before submitting]
AI grading is a suggestion. Always review the reasoning before accepting, especially for nuanced or context-dependent answers.
:::
## Related guides
- [How to Create an Exercise](/docs/create-exercise) — create and configure exercises
- [How to Create Certificates](/docs/certificates) — certificates that depend on exercise scores
- [Course](/docs/course) — course overview including the Submissions tab
- [How to Create an Exercise](/guides/create-exercise) — create and configure exercises
- [How to Create Certificates](/guides/certificates) — certificates that depend on exercise scores
- [Course](/course) — course overview including the Submissions tab
+45
View File
@@ -0,0 +1,45 @@
---
title: Guides
description: User guides for running your ClassroomIO academy, and developer guides for contributing to the project.
---
These guides cover everything from setup to day-to-day usage, split by who they're for.
## User Guides
Everything you need to build and run your academy — creating courses and exercises, running live
classes, enrolling students, publishing your landing pages, and managing your organization and team.
<CardGroup cols={2}>
<Card title="What is ClassroomIO" href="/what-is-classroomio" icon="circle-help">
The platform's key features and capabilities.
</Card>
<Card title="Quickstart" href="/quickstart/signup" icon="rocket">
Sign up and get your organization onboarded.
</Card>
<Card title="Create your first course" href="/guides/create-first-course" icon="graduation-cap">
Build a course end to end, from lessons to publishing.
</Card>
<Card title="Terminology" href="/terminology" icon="book-a">
The words ClassroomIO uses, and what they mean.
</Card>
</CardGroup>
## Developer Guides
ClassroomIO is open source and we welcome contributions. These cover getting the project running on
your machine, the conventions we follow, and the infrastructure behind the platform.
<CardGroup cols={2}>
<Card title="Contributor guides" href="/guides/contributing" icon="code">
Start here: local setup, contributing code, design, and translations.
</Card>
<Card title="Developers Guide" href="/guides/contributing/devs" icon="terminal">
Run the monorepo locally: prerequisites, install, and the apps you can start.
</Card>
</CardGroup>
:::note[Looking to self-host instead?]
Running ClassroomIO on your own infrastructure is covered separately in
[Self Hosting](/self-hosted).
:::
@@ -3,11 +3,9 @@ title: How to Invite Students
description: Learn how to invite students to your courses using email invitations and how enrollment works for free and paid courses.
---
import { Step, Steps } from 'fumadocs-ui/components/steps';
Course invitations in ClassroomIO let you add students to a course by sending them a secure enrollment link by email. Each invite is unique and can have its own expiry and usage limits.
For a full overview of enrollment paths (public enroll, invites, admin assignment, and paid access), see [How Course Enrollment Works](/docs/course-enrollment).
For a full overview of enrollment paths (public enroll, invites, admin assignment, and paid access), see [How Course Enrollment Works](/guides/course-enrollment).
## How course invitations work
@@ -80,7 +78,7 @@ Revoking does not remove students who have already joined; it only invalidates t
## Related guides
- [How Course Enrollment Works](/docs/course-enrollment) — enrollment paths, Allow New Students, and Explore visibility
- [Roles and Permissions](/docs/roles-and-permissions) — who can manage courses vs learn in the LMS
- [Set Up a Custom Domain](/docs/custom-domain) — which host appears in invite links
- [Student Dashboard](/docs/student-dashboard) — where students land after joining
- [How Course Enrollment Works](/guides/course-enrollment) — enrollment paths, Allow New Students, and Explore visibility
- [Roles and Permissions](/guides/roles-and-permissions) — who can manage courses vs learn in the LMS
- [Set Up a Custom Domain](/guides/custom-domain) — which host appears in invite links
- [Student Dashboard](/student-dashboard) — where students land after joining
@@ -3,14 +3,11 @@ title: Inviting a Team Member
description: Add admins and tutors to your organization by email invite or a reusable invite link.
---
import { Callout } from 'fumadocs-ui/components/callout';
import { Step, Steps } from 'fumadocs-ui/components/steps';
Team members are people who help you run your organization — **Admins** have full access, **Tutors** can manage the courses they are assigned to. Both are different from students, who use the LMS and cannot access the admin dashboard.
<Callout type="warn" title="Paid plan required">
Email invites and link invites for team members require a paid plan.
</Callout>
:::warning[Paid plan required]
Email invites and link invites for team members require a paid plan.
:::
To invite a team member, go to **Settings → Organization**, then click **Manage Team**.
@@ -70,6 +67,6 @@ Toggle the link off to stop accepting new members through it. You can re-enable
## Related guides
- [Roles and Permissions](/docs/roles-and-permissions) — what admins and tutors can do
- [How to Invite Students](/docs/invite-students) — invite students to a specific course (separate from team invites)
- [Organization](/docs/organization) — org settings overview
- [Roles and Permissions](/guides/roles-and-permissions) — what admins and tutors can do
- [How to Invite Students](/guides/invite-students) — invite students to a specific course (separate from team invites)
- [Organization](/organization) — org settings overview
@@ -3,16 +3,13 @@ title: Run a Live Class
description: Schedule a live session on a Live class course, set the join link, time, and timezone, and let ClassroomIO remind students and send a calendar invite.
---
import { Callout } from 'fumadocs-ui/components/callout';
import { Step, Steps } from 'fumadocs-ui/components/steps';
A **Live class** course runs around scheduled sessions you host on your own video tool (Zoom, Google Meet, Teams).
ClassroomIO handles everything around the session: the join link, the schedule, automatic reminders, a calendar invite,
and a one-click way for students to join.
## Set the course type
Set the course type to **Live class** in the course's **Settings** tab. See [Course Types](/docs/course-types)
Set the course type to **Live class** in the course's **Settings** tab. See [Course Types](/guides/course-types)
for what each type does.
## Schedule the session
@@ -20,7 +17,7 @@ for what each type does.
Each session lives on a lesson. Create the lesson for what you'll run live, then open its **Settings** tab and fill in
the **Live session** section.
![Lesson Settings tab showing the Live session fields for join link, date and time, and timezone](/live-class-session-settings.webp)
![Lesson Settings tab showing the Live session fields for join link, date and time, and timezone](/docs/live-class-session-settings.webp)
<Steps>
@@ -28,7 +25,7 @@ the **Live session** section.
### Create a lesson to run live
Add a lesson to the course for the session you'll hold live (for example, the webinar topic). See
[Creating Your First Course](/docs/create-first-course#adding-lessons) for adding lessons.
[Creating Your First Course](/guides/create-first-course#adding-lessons) for adding lessons.
</Step>
<Step>
@@ -69,18 +66,18 @@ Once a session is scheduled, enrolled students automatically receive:
- **A Join session button.** On their **LMS Home**, students see a **Join session** button on the course while a session
is upcoming, which opens the join link directly.
![LMS Home Live Sessions card with a Join session button](/live-class-lms-card.webp)
![LMS Home Live Sessions card with a Join session button](/docs/live-class-lms-card.webp)
In the course content sidebar, a live-session lesson shows a red live (broadcast) icon, so you and your students can spot
it at a glance.
![Student live session card showing the countdown and Add to calendar button](/live-class-student-card.webp)
![Student live session card showing the countdown and Add to calendar button](/docs/live-class-student-card.webp)
<Callout type="info">
:::info
Reminders are sent to enrolled students only. The times shown in emails and the calendar invite use the **Timezone** you
set on the session. The welcome email, reminders, and updates all reference the same calendar event, so students never
get a duplicate on their calendar.
</Callout>
:::
## Change the session time
@@ -89,24 +86,24 @@ saving asks you to confirm with **Notify students of the change?**. Choosing **S
student about the update with a fresh calendar invite that updates the existing event on their calendar instead of
creating a duplicate.
![Confirmation dialog asking to notify students of the session change](/live-class-change-time.webp)
![Confirmation dialog asking to notify students of the session change](/docs/live-class-change-time.webp)
<Callout type="warn">
:::warning
**Save and notify** emails all enrolled students. Use it when the time or link actually changed. For a first-time
schedule there is nothing to notify, so it saves without sending.
</Callout>
:::
## After the session
Pair the session with a post-session quiz and a certificate to round out the workflow:
- Add a graded quiz with [How to Create an Exercise](/docs/create-exercise), then use **Notify students**
- Add a graded quiz with [How to Create an Exercise](/guides/create-exercise), then use **Notify students**
to send everyone a link to take it.
- Issue completion certificates with [How to Create Certificates](/docs/certificates).
- Issue completion certificates with [How to Create Certificates](/guides/certificates).
## Related guides
- [Course Types](/docs/course-types): what Live class and the other types do
- [How to Create an Exercise](/docs/create-exercise): add a post-session quiz
- [How to Create Certificates](/docs/certificates): recognize completion
- [How Course Enrollment Works](/docs/course-enrollment): how students join the course
- [Course Types](/guides/course-types): what Live class and the other types do
- [How to Create an Exercise](/guides/create-exercise): add a post-session quiz
- [How to Create Certificates](/guides/certificates): recognize completion
- [How Course Enrollment Works](/guides/course-enrollment): how students join the course
@@ -3,16 +3,13 @@ title: Customize Your Academy Landing Page
description: Edit your academy landing page, including nav, hero buttons, footer, preview mode, and what the summary labels mean.
---
import { Callout } from 'fumadocs-ui/components/callout';
import { Step, Steps } from 'fumadocs-ui/components/steps';
Your **academy landing page** is the public homepage for your academy (for example `https://ciodevs.myclassroomio.com` or your custom domain). Students and prospects see it before they sign in. This guide covers the editor in **Settings → Landing Page**.
For org profile and subdomain setup, see [Customize Your Organization](/docs/customize-organization) and [Set Up a Custom Domain](/docs/custom-domain).
For org profile and subdomain setup, see [Customize Your Organization](/guides/customize-organization) and [Set Up a Custom Domain](/guides/custom-domain).
<Callout type="warn" title="Paid plan required">
Some landing themes require a paid plan. If the customize option is locked, upgrade or pick a theme included in your plan.
</Callout>
:::warning[Paid plan required]
Some landing themes require a paid plan. If the customize option is locked, upgrade or pick a theme included in your plan.
:::
## Open the landing page settings
@@ -50,11 +47,11 @@ The hero section supports a primary and optional secondary action.
| **Primary** (e.g. Get Started, Start Learning) | Often points to `/login` | Label and destination URL |
| **Secondary** (e.g. View Cohorts) | Optional second CTA | Label and destination URL |
Use your academy paths (for example `/login` or `/courses`) so visitors stay on your academy domain. After you verify a [custom domain](/docs/custom-domain), test buttons on the live site, not only in preview.
Use your academy paths (for example `/login` or `/courses`) so visitors stay on your academy domain. After you verify a [custom domain](/guides/custom-domain), test buttons on the live site, not only in preview.
<Callout type="warn" title="Preview disables link clicks">
The thumbnail preview on the Landing Page settings screen renders with links disabled. Use **Open Academy** or open your live academy URL to test buttons like **Get Started** and **View Cohorts**.
</Callout>
:::warning[Preview disables link clicks]
The thumbnail preview on the Landing Page settings screen renders with links disabled. Use **Open Academy** or open your live academy URL to test buttons like **Get Started** and **View Cohorts**.
:::
## Footer
@@ -75,8 +72,8 @@ Always validate hero and footer links on the live site before sharing the URL ex
## Related guides
- [Customize Your Organization](/docs/customize-organization) — org name, logo, brand color
- [Set Up a Custom Domain](/docs/custom-domain) — use your own domain on the landing page
- [Sharing Your Academy (Open Graph & Favicon)](/docs/academy-sharing-and-branding) — how shared links pick up title, description, and preview image
- [Create a Course Landing Page](/docs/course-landingpage) — per-course public pages (separate from the org homepage)
- [Student Dashboard](/docs/student-dashboard) — where students go after signing in
- [Customize Your Organization](/guides/customize-organization) — org name, logo, brand color
- [Set Up a Custom Domain](/guides/custom-domain) — use your own domain on the landing page
- [Sharing Your Academy (Open Graph & Favicon)](/guides/academy-sharing-and-branding) — how shared links pick up title, description, and preview image
- [Create a Course Landing Page](/guides/course-landingpage) — per-course public pages (separate from the org homepage)
- [Student Dashboard](/student-dashboard) — where students go after signing in
@@ -3,8 +3,6 @@ title: Roles and Permissions
description: Understand org roles in ClassroomIO, who can create courses, and how admin access differs from the student LMS.
---
import { Callout } from 'fumadocs-ui/components/callout';
ClassroomIO uses organization roles to control what each person can do. This guide covers the three built-in roles, who can create courses, and how that relates to course enrollment (which is controlled separately per course).
## Organization roles
@@ -15,11 +13,11 @@ ClassroomIO uses organization roles to control what each person can do. This gui
| **Tutor** | Instructors, content authors | Yes | No | Yes (courses they teach) |
| **Student** | Learners | No (LMS only) | No | No |
Admins have full control over organization settings, billing, domains, and all courses. Tutors can manage content for courses they are assigned to but cannot create new courses or change org-wide settings. Students use the [Student Dashboard](/docs/student-dashboard) to learn.
Admins have full control over organization settings, billing, domains, and all courses. Tutors can manage content for courses they are assigned to but cannot create new courses or change org-wide settings. Students use the [Student Dashboard](/student-dashboard) to learn.
<Callout type="info" title="Per-person enroll or create toggles">
ClassroomIO does not support per-person flags like "this user can create courses but that user cannot." Access is set by role. To limit who joins a **course**, use course settings, invites, and **People** instead. See [How Course Enrollment Works](/docs/course-enrollment).
</Callout>
:::info[Per-person enroll or create toggles]
ClassroomIO does not support per-person flags like "this user can create courses but that user cannot." Access is set by role. To limit who joins a **course**, use course settings, invites, and **People** instead. See [How Course Enrollment Works](/guides/course-enrollment).
:::
## Who can create courses
@@ -38,7 +36,7 @@ Students sign in on your academy and land in the LMS. Admins and tutors can swit
## Inviting team members
To add Admins or Tutors, use [Inviting a Team Member](/docs/invite-team-member). Team invites are separate from [student course invites](/docs/invite-students).
To add Admins or Tutors, use [Inviting a Team Member](/guides/invite-team-member). Team invites are separate from [student course invites](/guides/invite-students).
## Organization signup vs course enrollment
@@ -55,8 +53,8 @@ Configure org signup under **Settings → Organization**. Configure per-course a
## Related guides
- [How Course Enrollment Works](/docs/course-enrollment) — restrict who can join each course
- [How to Invite Students](/docs/invite-students) — course enrollment links
- [Inviting a Team Member](/docs/invite-team-member) — add admins and tutors
- [Student Dashboard](/docs/student-dashboard) — what students see in the LMS
- [Organization](/docs/organization) — org settings overview
- [How Course Enrollment Works](/guides/course-enrollment) — restrict who can join each course
- [How to Invite Students](/guides/invite-students) — course enrollment links
- [Inviting a Team Member](/guides/invite-team-member) — add admins and tutors
- [Student Dashboard](/student-dashboard) — what students see in the LMS
- [Organization](/organization) — org settings overview
@@ -3,8 +3,6 @@ title: How to Take Attendance
description: Record lesson attendance for each student in a course using the Attendance tab.
---
import { Callout } from 'fumadocs-ui/components/callout';
The **Attendance** tab in a course lets you track which students attended each lesson. Only students who are enrolled in the course appear in the attendance grid — enroll them first via **People** if they are missing. It displays a grid with students as rows and lessons as columns. A checked box means the student was present; unchecked means absent.
## Mark attendance
@@ -18,9 +16,9 @@ Changes save immediately — there is no separate save button.
Use the search bar to filter students by name if the course has a large group.
<Callout type="info" title="Students see read-only attendance">
Students can view their own attendance record in the Attendance tab when they open the course in the LMS, but they cannot edit it.
</Callout>
:::info[Students see read-only attendance]
Students can view their own attendance record in the Attendance tab when they open the course in the LMS, but they cannot edit it.
:::
## Pagination
@@ -28,6 +26,6 @@ The table shows 10 students per page. Use the page controls at the bottom to mov
## Related guides
- [Creating Your First Course](/docs/create-first-course) — adding lessons to a course
- [How Course Enrollment Works](/docs/course-enrollment) — how students join a course
- [Course](/docs/course) — course overview and available tabs
- [Creating Your First Course](/guides/create-first-course) — adding lessons to a course
- [How Course Enrollment Works](/guides/course-enrollment) — how students join a course
- [Course](/course) — course overview and available tabs
@@ -3,8 +3,6 @@ title: Use Math in the Editor
description: Add and edit LaTeX math blocks and inline expressions inside the ClassroomIO rich text editor.
---
import { Step, Steps } from 'fumadocs-ui/components/steps';
ClassroomIO’s rich text editor includes built-in math support with KaTeX rendering. You can insert inline expressions, add block equations, and edit them later without extra setup.
<Steps>
@@ -3,9 +3,6 @@ title: Customize the Welcome Email
description: Write a custom message that students receive automatically when they enroll in a course.
---
import { Callout } from 'fumadocs-ui/components/callout';
import { Step, Steps } from 'fumadocs-ui/components/steps';
Every course can send a personalized welcome email when a student enrolls. Use it to greet students, share what to
expect, or include a link such as a live session URL.
@@ -34,28 +31,28 @@ Save the course settings. Your message is now used for new enrollments.
</Steps>
![Course Settings Welcome email rich text editor](/welcome-email-editor.webp)
![Course Settings Welcome email rich text editor](/docs/welcome-email-editor.webp)
## How it sends
The welcome email goes out automatically whenever a student enrolls, whether they self-enroll, accept an invite, or are
added from the audience. Your message appears in the email above the standard "Sign in to the LMS" link.
<Callout type="info">
:::info
Leave the **Welcome email** blank to use ClassroomIO's default welcome message. Formatting is cleaned for safe email
delivery, so some advanced styling may be simplified.
</Callout>
:::
## Live class courses: automatic session invite
For a **Live class** course with a scheduled session, the welcome email also attaches a calendar invite for the next
session, so students can add it to their calendar the moment they enroll. You do not set this up here; it is pulled from
the session you scheduled on the lesson. See [Run a Live Class](/docs/live-class).
the session you scheduled on the lesson. See [Run a Live Class](/guides/live-class).
![Rendered welcome email showing the Sign in to the LMS link and calendar invite attachment](/welcome-email-preview.webp)
![Rendered welcome email showing the Sign in to the LMS link and calendar invite attachment](/docs/welcome-email-preview.webp)
## Related guides
- [How to Invite Students](/docs/invite-students): add students to a course
- [How Course Enrollment Works](/docs/course-enrollment): the enrollment flow
- [Run a Live Class](/docs/live-class): include your session link in the welcome message
- [How to Invite Students](/guides/invite-students): add students to a course
- [How Course Enrollment Works](/guides/course-enrollment): the enrollment flow
- [Run a Live Class](/guides/live-class): include your session link in the welcome message
-25
View File
@@ -1,25 +0,0 @@
---
title: Welcome to ClassroomIO
description: Learn how to create customer academies, partner certification programs, and employee training workflows in ClassroomIO.
---
<img
alt="ClassroomIO documentation for customer, partner, and employee education"
src="https://brand.cdn.clsrio.com/og/classroomio-opengraph.jpg"
/>
Welcome to the ClassroomIO documentation—your comprehensive guide to streamlining training for everyone. Our all-in-one platform empowers bootcamps, educators, and businesses to manage training programs easily. Run multiple classes and cohorts from a single UI with our mobile-first application, ensuring students can access all published content from any device.
## What You'll Find Here
This documentation covers everything from setup to usage, including:
- **Core Features**: Well-detailed explanations of ClassroomIO's capabilities and how to take advantage of them
- **Developer & Designer Resources**: Everything needed to successfully contribute to the project
- **Practical Guides**: From simple functionalities to complex use cases
If something isn't covered in this documentation, please feel free to [open an issue](https://github.com/classroomio/classroomio/issues) and we'll address it.
## Get Started
Ready to begin? Check out our [What is ClassroomIO](/docs/what-is-classroomio) page to learn about the platform's key features and capabilities.
+27
View File
@@ -0,0 +1,27 @@
---
title: Welcome to ClassroomIO
description: Documentation for ClassroomIO — guides for using the platform, contributing to it, and self-hosting it.
---
<img
alt="ClassroomIO documentation for customer, partner, and employee education"
src="https://assets.cdn.clsrio.com/www/home/ai-course-builder.jpeg"
/>
Welcome to the ClassroomIO documentation. ClassroomIO is an all-in-one platform for bootcamps,
educators, and businesses to run training programs — multiple classes and cohorts from a single,
mobile-first UI.
## Where to go next
<CardGroup cols={2}>
<Card title="Guides" href="/guides" icon="book-open">
User guides for building and running your academy, plus developer guides for contributing.
</Card>
<Card title="Self Hosting" href="/self-hosted" icon="server">
Run ClassroomIO on your own infrastructure with Docker, Railway, Coolify, or Dokploy.
</Card>
</CardGroup>
If something isn't covered in this documentation, please
[open an issue](https://github.com/classroomio/classroomio/issues) and we'll address it.
+5 -7
View File
@@ -3,8 +3,6 @@ title: MCP
description: Connect Claude Code, Codex, Cursor, OpenCode, and other MCP clients to ClassroomIO for draft-based course authoring.
---
import { Callout } from 'fumadocs-ui/components/callout';
`@classroomio/mcp` is a thin stdio MCP server for ClassroomIO course authoring.
It does not parse PDFs or generate course structures itself. The agent does that work. The MCP
@@ -475,8 +473,8 @@ For broad revisions, prefer this sequence:
That keeps major AI-assisted edits reviewable and explicit.
<Callout title="Operational Rule">
Keep published drafts for history and auditability. Do not treat omission from a draft as an
instruction to delete live sections or lessons. Destructive changes should only happen through
explicit user intent.
</Callout>
:::note[Operational Rule]
Keep published drafts for history and auditability. Do not treat omission from a draft as an
instruction to delete live sections or lessons. Destructive changes should only happen through
explicit user intent.
:::
-61
View File
@@ -1,61 +0,0 @@
{
"pages": [
"---Introduction---",
"home",
"what-is-classroomio",
"terminology",
"quickstart",
"---Core Features---",
"course",
"cohorts",
"api",
"mcp",
"tags",
"audience",
"community",
"organization",
"student-dashboard",
"---Enterprise---",
"enterprise-sso",
"token-auth",
"---Get started---",
"create-first-course",
"---Build a course---",
"course-types",
"create-exercise",
"grade-exercise",
"course-progression",
"use-math-in-editor",
"certificates",
"---Live classes---",
"live-class",
"take-attendance",
"---Enrollment & students---",
"course-enrollment",
"welcome-email",
"invite-students",
"---Publish your academy---",
"course-landingpage",
"org-landing-page",
"academy-sharing-and-branding",
"custom-domain",
"---Organization & team---",
"customize-organization",
"invite-team-member",
"roles-and-permissions",
"---Contributor guides---",
"contributor-guides",
"---Self Hosting---",
"self-hosted"
]
}
+19 -21
View File
@@ -3,40 +3,38 @@ title: Organization
description: Overview of organizations in ClassroomIO — settings, branding, domains, roles, and related guides.
---
import { Callout } from 'fumadocs-ui/components/callout';
An **organization** in ClassroomIO holds courses, people, branding, and your academy. Every admin and student belongs to an organization. This page points you to the right guides for setup and day-to-day management.
## What you configure at org level
| Area | Where | Guide |
| --- | --- | --- |
| Name, logo, brand color | **Settings → Organization** | [Customize Your Organization](/docs/customize-organization) |
| Public homepage | **Settings → Landing Page** | [Customize Your Academy Landing Page](/docs/org-landing-page) |
| Custom domain | **Settings → Organization** | [Set Up a Custom Domain](/docs/custom-domain) |
| Team roles | **Settings → Organization** | [Roles and Permissions](/docs/roles-and-permissions), [Inviting a Team Member](/docs/invite-team-member) |
| Signup rules, Internal Enrollment Only | **Settings → Authentication → General** | [How Course Enrollment Works](/docs/course-enrollment) (org access section) |
| Name, logo, brand color | **Settings → Organization** | [Customize Your Organization](/guides/customize-organization) |
| Public homepage | **Settings → Landing Page** | [Customize Your Academy Landing Page](/guides/org-landing-page) |
| Custom domain | **Settings → Organization** | [Set Up a Custom Domain](/guides/custom-domain) |
| Team roles | **Settings → Organization** | [Roles and Permissions](/guides/roles-and-permissions), [Inviting a Team Member](/guides/invite-team-member) |
| Signup rules, Internal Enrollment Only | **Settings → Authentication → General** | [How Course Enrollment Works](/guides/course-enrollment) (org access section) |
## Default URL
On creation, each org gets `https://<siteName>.myclassroomio.com` as its academy URL. You can connect a [custom domain](/docs/custom-domain) so students use your own hostname.
On creation, each org gets `https://<siteName>.myclassroomio.com` as its academy URL. You can connect a [custom domain](/guides/custom-domain) so students use your own hostname.
<Callout type="info" title="Academy vs admin app">
Share your academy URL with students. The generic `app.classroomio.com` host is for platform access when no org subdomain applies. Course and enroll links should use your academy when possible.
</Callout>
:::info[Academy vs admin app]
Share your academy URL with students. The generic `app.classroomio.com` host is for platform access when no org subdomain applies. Course and enroll links should use your academy when possible.
:::
## Courses and cohorts inside an org
- **Courses** are individual learning containers. See [Course](/docs/course).
- **Cohorts** group courses with shared goals. See [Cohorts](/docs/cohorts).
- **Students** use the [Student Dashboard](/docs/student-dashboard) after they sign in.
- **Courses** are individual learning containers. See [Course](/course).
- **Cohorts** group courses with shared goals. See [Cohorts](/cohorts).
- **Students** use the [Student Dashboard](/student-dashboard) after they sign in.
## Related guides
- [Customize Your Organization](/docs/customize-organization)
- [Customize Your Academy Landing Page](/docs/org-landing-page)
- [Set Up a Custom Domain](/docs/custom-domain)
- [Roles and Permissions](/docs/roles-and-permissions)
- [How Course Enrollment Works](/docs/course-enrollment)
- [Course](/docs/course)
- [Cohorts](/docs/cohorts)
- [Customize Your Organization](/guides/customize-organization)
- [Customize Your Academy Landing Page](/guides/org-landing-page)
- [Set Up a Custom Domain](/guides/custom-domain)
- [Roles and Permissions](/guides/roles-and-permissions)
- [How Course Enrollment Works](/guides/course-enrollment)
- [Course](/course)
- [Cohorts](/cohorts)
@@ -1,4 +0,0 @@
{
"signup": "Sign Up",
"onboarding": "Onboarding"
}
+7 -9
View File
@@ -3,9 +3,7 @@ title: Sign Up for ClassroomIO
description: Get started with ClassroomIO by signing up for an account.
---
import { Step, Steps } from 'fumadocs-ui/components/steps';
If you're new to ClassroomIO, you must create an account to set up and launch your organization. By signing up, you'll gain access to all the [exciting features we offer](/docs/what-is-classroomio#key-features) to supercharge your classroom.
If you're new to ClassroomIO, you must create an account to set up and launch your organization. By signing up, you'll gain access to all the [exciting features we offer](/what-is-classroomio#key-features) to supercharge your classroom.
<Steps>
<Step>
@@ -13,7 +11,7 @@ If you're new to ClassroomIO, you must create an account to set up and launch yo
Navigate to [app.classroomio.com](https://app.classroomio.com) and click the **Signup** button at the top right corner of the page.
![ClassroomIO website with a navigation bar at the top of the page containing the logo on the left along with the login and signup button on the right. Also, the page contains an email field with a Join waiting list button.](/dashboard-homepage-signedout.webp)
![ClassroomIO website with a navigation bar at the top of the page containing the logo on the left along with the login and signup button on the right. Also, the page contains an email field with a Join waiting list button.](/docs/dashboard-homepage-signedout.webp)
</Step>
@@ -24,7 +22,7 @@ On the signup page, enter your **valid email address**. Next, create a secure pa
You can also sign up using your Google account.
![ClassroomIO sign-up page with a sign-up form containing inputs for Your Name, Your Password, and, Confirm Password. At the bottom, there is a Create Account button, a Sign up with Google button, and, a link to log in if you already have an account.](/signup-page.webp)
![ClassroomIO sign-up page with a sign-up form containing inputs for Your Name, Your Password, and, Confirm Password. At the bottom, there is a Create Account button, a Sign up with Google button, and, a link to log in if you already have an account.](/docs/signup-page.webp)
</Step>
@@ -33,11 +31,11 @@ You can also sign up using your Google account.
After completing the Signup form, you'll be redirected to the **Onboarding** page. There, you'll provide your **Full Name** and **Name of Organization.** As you enter your organization name, the **Organization Site Name** field will auto-fill. To proceed, click **Continue**.
![ClassroomIO onboarding page with a form containing inputs for Full Name, Name of Organization, and, Organization Site Name. At the bottom, there is a Continue button.](/onboarding-page1.webp)
![ClassroomIO onboarding page with a form containing inputs for Full Name, Name of Organization, and, Organization Site Name. At the bottom, there is a Continue button.](/docs/onboarding-page1.webp)
Next, you'll complete a brief survey regarding what brought you to ClassroomIO and how you discovered our platform. Your responses will help us tailor ClassroomIO's features to best suit your needs. Finally, click **Continue** to submit.
![ClassroomIO second slide of onboarding page with a questionnaire](/onboarding-page2.webp)
![ClassroomIO second slide of onboarding page with a questionnaire](/docs/onboarding-page2.webp)
</Step>
@@ -46,14 +44,14 @@ Next, you'll complete a brief survey regarding what brought you to ClassroomIO a
You are successfully signed up and ready to use ClassroomIO when you see the dashboard.
![ClassroomIO dashboard with a big pop-up containing a welcome message](/dashboard-homepage-signedin.webp)
![ClassroomIO dashboard with a big pop-up containing a welcome message](/docs/dashboard-homepage-signedin.webp)
</Step>
<Step>
### What Next?
After signing up successfully, you can now set up and [customize your Organization](/docs/customize-organization).
After signing up successfully, you can now set up and [customize your Organization](/guides/customize-organization).
</Step>
</Steps>
@@ -0,0 +1,41 @@
---
title: CSP / allowed domains
description: Allow the external CDN, font, media, and frame domains your self-hosted dashboard needs to load.
---
When `PUBLIC_IS_SELFHOSTED=true`, the dashboard ships a strict Content Security Policy that allows only
`'self'`. If you load assets from external domains — a CDN, custom fonts, embedded video, an external
media/storage host — you must add those domains, or the browser blocks them.
## Variables
Set the broad override, or the per-directive variables for finer control. All are **comma-separated**
lists of domains.
| Variable | Description |
|----------|-------------|
| `ALLOWED_EXTERNAL_DOMAINS` | Overrides all per-directive vars below with one allowlist |
| `CSP_SCRIPT_SRC_DOMAINS` | Domains allowed in `script-src` |
| `CSP_STYLE_SRC_DOMAINS` | Domains allowed in `style-src` |
| `CSP_CONNECT_SRC_DOMAINS` | Domains allowed in `connect-src` |
| `CSP_FRAME_SRC_DOMAINS` | Domains allowed in `frame-src` |
| `CSP_FONT_SRC_DOMAINS` | Domains allowed in `font-src` |
| `CSP_MEDIA_SRC_DOMAINS` | Domains allowed in `img-src` and `media-src` |
```zsh title=".env"
# One broad allowlist…
ALLOWED_EXTERNAL_DOMAINS=https://fonts.googleapis.com,https://storage.yourdomain.com
# …or per-directive, e.g. just your object-storage host for uploaded media:
CSP_MEDIA_SRC_DOMAINS=https://storage.yourdomain.com
```
:::note[Runtime — no rebuild needed]
CSP domain variables are read at **container startup**. Change them and restart the container — you do
not need to rebuild any image.
:::
:::warning[You don't need to add the API]
Normal dashboard traffic is proxied to the API **same-origin**, so the API host does not need to be in
the CSP. Add external hosts only — your CDN, video provider, font host, and object-storage/media domain
(the most common one, so browsers can load uploaded media).
:::
@@ -0,0 +1,38 @@
---
title: Email (SMTP)
description: Configure SMTP so ClassroomIO can send signup verification, password reset, and invite emails.
---
ClassroomIO sends transactional email — **signup verification, password resets, and invite emails** —
over SMTP. Without it the app still runs, but it can't send any of those messages, so have mail
credentials ready before you deploy.
## Variables
| Variable | Description |
|----------|-------------|
| `SMTP_HOST` | SMTP server hostname |
| `SMTP_PORT` | SMTP server port (commonly `587`) |
| `SMTP_USER` | SMTP username |
| `SMTP_PASSWORD` | SMTP password |
| `SMTP_SENDER` | From address, e.g. `ClassroomIO <you@yourdomain.com>` |
```zsh title=".env"
SMTP_HOST=smtp.yourprovider.com
SMTP_PORT=587
SMTP_USER=<smtp username>
SMTP_PASSWORD=<smtp password>
SMTP_SENDER=ClassroomIO <you@yourdomain.com>
```
:::note[Which services need SMTP]
The **jobs** worker sends the mail, and the **api** reads the same values. On Docker they come from the
single root `.env`; on Railway set them on `cio-jobs` (and `cio-api`); on Coolify they apply to the
whole compose stack. The jobs worker won't send invites or notifications if SMTP is unset.
:::
## Providers
Any standard SMTP provider works — including transactional services such as **Zoho / ZeptoMail**,
Postmark, Resend (SMTP), Amazon SES, or Mailgun. See the
[Nodemailer SMTP docs](https://nodemailer.com/smtp/) for provider-specific setup and port/TLS options.
@@ -0,0 +1,47 @@
---
title: Configuration
description: Shared configuration reference for self-hosting ClassroomIO — secrets, email, storage, CSP, integrations, licensing, and versions.
---
However you deploy ClassroomIO — [Docker](/self-hosted/docker), [Railway](/self-hosted/railway), or
[Coolify](/self-hosted/coolify) — the stack reads the **same set of environment variables**. This
section is the single source of truth for what each group of variables does. The provider guides show
you *where* to set them for that platform; these pages explain *what* to set and *why*.
## How to read these pages
ClassroomIO is configured entirely through environment variables. On Docker they live in one root
`.env` file that every `docker compose` command reads via `--env-file .env`; on Railway and Coolify you
set the same variables on each service.
:::note[Value = required, blank = optional]
Throughout these pages and the provider guides, a variable shown **with a value** is one you should
set for a working deployment. A variable left **blank after `=`** is optional — fill it in only if you
use that feature. Leaving an optional variable blank never causes a build or boot error.
:::
## Configuration topics
<CardGroup cols={2}>
<Card title="Environment & secrets" href="/self-hosted/configuration/secrets" icon="key-round">
Generate `BETTER_AUTH_SECRET` and `PRIVATE_SERVER_KEY` — the stack won't boot without them.
</Card>
<Card title="Email (SMTP)" href="/self-hosted/configuration/email" icon="mail">
SMTP for signup verification, password resets, and invite emails.
</Card>
<Card title="Object storage & uploads" href="/self-hosted/configuration/storage" icon="hard-drive">
MinIO or S3/R2 for file and video uploads, plus upload size limits.
</Card>
<Card title="CSP / allowed domains" href="/self-hosted/configuration/csp" icon="shield">
Allow the CDN, font, and media domains your dashboard needs to load.
</Card>
<Card title="Integrations" href="/self-hosted/configuration/integrations" icon="plug">
Google OAuth, Unsplash, OpenAI transcription and AI course generation.
</Card>
<Card title="SSO & licensing" href="/self-hosted/configuration/sso" icon="id-card">
Set `LICENSE_KEY` to unlock Enterprise features such as SSO.
</Card>
<Card title="Versions & upgrades" href="/self-hosted/configuration/versions" icon="git-branch">
Pin `CIO_VERSION`, upgrade safely, and roll back.
</Card>
</CardGroup>
@@ -0,0 +1,48 @@
---
title: Integrations
description: Optional integrations for self-hosted ClassroomIO — Google OAuth, Unsplash, OpenAI, and branding.
---
These integrations are all **optional** — ClassroomIO runs without any of them. Add a variable only if
you use that feature; leaving it blank never causes a build or boot error.
## Google OAuth
Let users sign in with Google.
| Variable | Description |
|----------|-------------|
| `GOOGLE_CLIENT_ID` | Google OAuth client ID |
| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret |
## OpenAI (transcription & AI course generation)
Powers video transcription and AI course generation. These jobs run on the **jobs** worker, so set the
key wherever the worker reads its environment.
| Variable | Description |
|----------|-------------|
| `OPENAI_API_KEY` | OpenAI API key — transcription + AI course generation |
## Unsplash
Enables the in-app Unsplash image search.
| Variable | Description |
|----------|-------------|
| `UNSPLASH_API_KEY` | Unsplash API access key |
## Branding & routing
| Variable | Description |
|----------|-------------|
| `PUBLIC_APP_TITLE` | Custom app title |
| `PUBLIC_APP_DESCRIPTION` | Custom app description / meta tag |
| `PUBLIC_OG_IMAGE_URL` | Custom Open Graph image URL |
| `PRIVATE_APP_HOST` | Dashboard host for multi-subdomain routing |
| `PRIVATE_APP_SUBDOMAINS` | Subdomains served by the dashboard |
:::note[Enterprise features]
`LICENSE_KEY` unlocks Enterprise-only integrations such as SSO — see
[SSO & licensing](/self-hosted/configuration/sso).
:::
@@ -0,0 +1,56 @@
---
title: Environment & secrets
description: Generate the two secrets ClassroomIO needs to boot — BETTER_AUTH_SECRET and PRIVATE_SERVER_KEY.
---
ClassroomIO needs two secrets before it will run. Generate them once, keep them safe, and reuse the
same values across the services that need them.
## The two secrets
```zsh title="Terminal"
openssl rand -hex 32 # BETTER_AUTH_SECRET
openssl rand -hex 32 # PRIVATE_SERVER_KEY (must be identical in the api and dashboard)
```
| Variable | What it's for | Where it goes |
|----------|---------------|---------------|
| `BETTER_AUTH_SECRET` | Signs authentication sessions. | The **api** service. |
| `PRIVATE_SERVER_KEY` | Authenticates server-side dashboard → API requests. | **Both** the api and dashboard — the value must be **identical** in each. |
:::danger[The stack won't start without them]
The compose file **fails fast** if `BETTER_AUTH_SECRET` or `PRIVATE_SERVER_KEY` is empty — the
containers refuse to boot rather than run with a predictable, guessable session-signing secret. Set
both to a strong random value (e.g. `openssl rand -hex 32`).
:::
:::note[Who generates them]
Only the source-build path's `run-docker-full-stack.sh` auto-generates these when missing. The
pre-built image path, Railway, and Coolify do **not** run that script — you generate the secrets
yourself and paste them in.
:::
## Database credentials
Postgres runs inside the stack (or as a managed plugin on Railway). Set a strong password — the
defaults are for local use only.
| Variable | Description | Default |
|----------|-------------|---------|
| `POSTGRES_DB` | Database name | `classroomio` |
| `POSTGRES_USER` | Database user | `postgres` |
| `POSTGRES_PASSWORD` | Database password — set a **strong** value in production | `postgres` |
## Mode & routing basics
| Variable | Description | Default |
|----------|-------------|---------|
| `PUBLIC_IS_SELFHOSTED` | Self-hosted mode flag | `true` |
| `PRIVATE_SERVER_URL` | Internal API URL for server-side rendering (over the container network) | `http://api:3081` |
| `DASHBOARD_ORIGIN` | Public dashboard origin, used as the auth base URL and in invite/email links | `http://localhost:3082` |
:::warning[Use an `https://` origin in production]
`DASHBOARD_ORIGIN` (and `ORIGIN` on the dashboard) must be the real `https://` URL your users visit —
auth session cookies are `Secure`, so login silently fails over plain HTTP on any public domain. See
[Self Hosting → HTTPS is required in production](/self-hosted#https-is-required-in-production).
:::
@@ -0,0 +1,34 @@
---
title: SSO & licensing
description: Set LICENSE_KEY to unlock Enterprise features such as SSO on your self-hosted ClassroomIO.
---
Enterprise-only features — **SSO**, **token auth**, and no-tracking mode — require an Enterprise
**license key**. If you don't use any Enterprise feature, you don't need a key; leave `LICENSE_KEY`
unset and everything else works.
## Setting `LICENSE_KEY`
`LICENSE_KEY` is read by the **api** service. Set it wherever that service takes its environment:
| Deployment | Where to set it |
|------------|-----------------|
| Local development | `apps/api/.env` |
| Docker Compose | repository root `.env` |
| Railway | the `cio-api` service environment |
| Coolify | the resource's environment variables |
```zsh title=".env"
LICENSE_KEY=<your enterprise license key>
```
## Getting a license
[Book a call](http://cal.com/classroomio/enterprise) to get an Enterprise license key.
## Feature docs
Once the key is set, configure the features themselves:
- [Enterprise SSO](/enterprise-sso) — Single Sign-On with OIDC (Okta, Azure AD, and more).
- [Token Auth](/token-auth) — issue signed tokens to log users in from your own app.
@@ -0,0 +1,74 @@
---
title: Object storage & uploads
description: Configure object storage (MinIO or S3-compatible) for file and video uploads, plus upload size limits.
---
ClassroomIO stores uploaded files and videos in **object storage**. You have two choices: run the
**bundled MinIO** alongside the stack, or point ClassroomIO at an **external S3-compatible** service
(AWS S3, Cloudflare R2, etc.). Both speak the S3 API and use the same `OBJECT_STORAGE_*` variables.
## Option 1 — Bundled MinIO (default)
The Docker and Coolify compose files include **MinIO** under the `minio` profile, and the startup
script auto-configures it. You only change these for custom credentials.
| Variable | Default |
|----------|---------|
| `MINIO_ROOT_USER` | randomized by the script (`minioadmin` if you skip it) |
| `MINIO_ROOT_PASSWORD` | randomized by the script (`minioadmin` if you skip it) |
| `OBJECT_STORAGE_ENDPOINT` | `http://minio:9000` |
| `OBJECT_STORAGE_PUBLIC_ENDPOINT` | `http://localhost:9000` |
| `OBJECT_STORAGE_ACCESS_KEY_ID` | matches `MINIO_ROOT_USER` |
| `OBJECT_STORAGE_SECRET_ACCESS_KEY` | matches `MINIO_ROOT_PASSWORD` |
| `OBJECT_STORAGE_FORCE_PATH_STYLE` | `true` (required for MinIO) |
| `OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL` | `http://localhost:9000/media` |
:::warning[MinIO is your durability boundary]
Single-node MinIO has no redundancy — its volume is the only thing protecting your uploads. Give it
**persistent storage** and **back it up**. If you'd rather not operate storage yourself, use external
**Cloudflare R2 / AWS S3** instead (R2 has no egress fees) — see below.
:::
## Option 2 — External S3 / Cloudflare R2
The `OBJECT_STORAGE_*` variables speak the S3 API, so they work with **any S3-compatible provider**,
including Cloudflare R2 and AWS S3. Point them at the provider and start **without** the bundled MinIO
(`./run-docker-full-stack.sh --no-minio`, or leave the `minio` profile off).
| Variable | Description |
|----------|-------------|
| `OBJECT_STORAGE_ENDPOINT` | Provider endpoint the app uploads to (for R2: `https://<account>.r2.cloudflarestorage.com`) |
| `OBJECT_STORAGE_PUBLIC_ENDPOINT` | Public endpoint browsers download from |
| `OBJECT_STORAGE_ACCESS_KEY_ID` / `OBJECT_STORAGE_SECRET_ACCESS_KEY` | Provider credentials |
| `OBJECT_STORAGE_REGION` | Region (some providers require it; `auto` for R2) |
| `OBJECT_STORAGE_FORCE_PATH_STYLE` | `true` for MinIO; `false` for R2 |
| `OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL` | Public base URL for media (`…/media`) |
| `OBJECT_STORAGE_BUCKET_VIDEOS` / `_DOCUMENTS` / `_MEDIA` | Bucket names (default `videos`, `documents`, `media`) |
:::note[Legacy `CLOUDFLARE_*` variables]
ClassroomIO also accepts a legacy Cloudflare R2 config (`CLOUDFLARE_ACCOUNT_ID`, `CLOUDFLARE_ACCESS_KEY`,
`CLOUDFLARE_SECRET_ACCESS_KEY`, …). It is a **fallback** used only when the `OBJECT_STORAGE_*` variables
are absent — prefer `OBJECT_STORAGE_*` for new deployments (it covers R2 too). For step-by-step R2 bucket
and CORS setup, see the [Cloudflare Setup](/guides/contributing/cloudflare-setup) guide.
:::
## Upload file size limits
Set these to raise or lower upload caps (values are in **megabytes**). Compose passes them through to
the `api` and `dashboard` containers. Unset = built-in defaults. On [Railway](/self-hosted/railway) or
[Coolify](/self-hosted/coolify), set the **same** values on both the API and dashboard services.
| Variable | Default | Applies to |
|----------|---------|------------|
| `UPLOAD_MAX_DOCUMENT_MB` | 5 | Course lesson documents |
| `UPLOAD_MAX_IMAGE_MB` | 5 | Avatars, media library, editor images |
| `UPLOAD_MAX_VIDEO_MB` | 800 | Lesson video uploads |
| `UPLOAD_MAX_EXERCISE_FILE_MB` | 2 | Student assignment file-upload questions |
| `UPLOAD_MAX_AGENT_DOCUMENT_MB` | 5 | AI assistant document uploads |
| `UPLOAD_MAX_LANDING_IMAGE_MB` | 0.5 | Org landing page cover images |
| `UPLOAD_MAX_THUMBNAIL_MB` | 5 | Media thumbnail uploads |
:::warning[Reverse proxy limits]
If you raise these values, also increase your reverse proxy body-size limit (e.g. nginx
`client_max_body_size`) so large uploads aren't rejected before they reach ClassroomIO.
:::
@@ -0,0 +1,53 @@
---
title: Versions & upgrades
description: Pin CIO_VERSION, upgrade ClassroomIO safely, and roll back to a previous release.
---
ClassroomIO publishes versioned images. `CIO_VERSION` controls which tag you run.
## Choosing a tag
- **Pin an exact release** (e.g. `1.0.0`) in production, so a merge to `main` never restarts you onto
changed images unattended.
- `latest` is the freshest `main` build — it moves on **every merge** (rolling; may include unreleased
code). The version tags (`1.0.0`, `1.0`, `1`) are the immutable, pinned releases.
```zsh title=".env"
CIO_VERSION=1.0.0 # pin a release; blank or "latest" = rolling main build
```
:::note[Confirm the tag exists]
Before deploying, confirm the tag you set actually exists at
[hub.docker.com/r/classroomio](https://hub.docker.com/r/classroomio/).
:::
## Upgrading
### Pre-built images (Docker)
Bump `CIO_VERSION` in `.env`, then re-pull and recreate:
```zsh title="Terminal"
docker compose -f docker-compose.images.yaml --env-file .env --profile minio pull
docker compose -f docker-compose.images.yaml --env-file .env --profile minio up -d
```
### Build from source (Docker)
`git pull`, then re-run `./run-docker-full-stack.sh --build` to rebuild.
### Railway / Coolify
Set `CIO_VERSION` to the newer release and redeploy. Because you pin a version, a new release never
restarts you onto changed images unattended.
## Rolling back
Set `CIO_VERSION` back to the previous release and run the same upgrade command (or redeploy). Pinned
versions make rollbacks reproducible.
:::warning[Back up before upgrading]
Database schema migrations run **automatically** when the API starts. Back up your Postgres volume
(and object storage) before a major upgrade, and avoid `docker compose down -v` / `docker volume prune`,
which delete your data volumes.
:::
+59 -64
View File
@@ -3,9 +3,6 @@ title: Coolify
description: Deploy ClassroomIO on Coolify using the pull-based Docker Compose stack, with a video walkthrough.
---
import { Callout } from 'fumadocs-ui/components/callout';
import { Step, Steps } from 'fumadocs-ui/components/steps';
[Coolify](https://coolify.io) is a self-hosted PaaS (an open-source Heroku/Netlify alternative) you run on your own VPS. It runs ClassroomIO from the published `classroomio/*` images — your Coolify host **pulls** them and never builds anything.
## Watch the walkthrough
@@ -13,9 +10,7 @@ import { Step, Steps } from 'fumadocs-ui/components/steps';
Prefer to follow along? This video walks through the whole Coolify deployment end to end. The written steps below mirror it, and the [Environment variable reference](#environment-variable-reference) has every value to copy.
{/* TODO: replace REPLACE_WITH_VIDEO_ID with the Coolify walkthrough video id once recorded */}
<div style={{ display: 'flex', justifyContent: 'center', marginTop: '16px', marginBottom: '16px' }}>
<iframe width="560" height="315" src="https://www.youtube.com/embed/5pwzUn8JBws?si=HcetU4pDYmRbwn4M" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div>
<YouTube id="5pwzUn8JBws" />
## Prerequisites
@@ -24,15 +19,17 @@ Prefer to follow along? This video walks through the whole Coolify deployment en
- An **SMTP provider** (or Zoho/ZeptoMail) for signup verification, password reset, and invite emails. See [Nodemailer SMTP docs](https://nodemailer.com/smtp/).
- *Optional:* an **OpenAI API key** (transcription + AI course generation) and Google OAuth / Unsplash keys.
<Callout title="Two secrets to generate first">
You'll reuse `BETTER_AUTH_SECRET` and `PRIVATE_SERVER_KEY` across services. Coolify does **not**
run our secret-generation script, so generate them once yourself and keep them handy:
:::note[Two secrets to generate first]
You'll reuse `BETTER_AUTH_SECRET` and `PRIVATE_SERVER_KEY` across services. Coolify does **not**
run our secret-generation script, so generate them once yourself and keep them handy:
```zsh title="Terminal"
openssl rand -hex 32 # BETTER_AUTH_SECRET
openssl rand -hex 32 # PRIVATE_SERVER_KEY (must be identical in the api and dashboard services)
```
</Callout>
```zsh title="Terminal"
openssl rand -hex 32 # BETTER_AUTH_SECRET
openssl rand -hex 32 # PRIVATE_SERVER_KEY (must be identical in the api and dashboard services)
```
See [Configuration → Environment & secrets](/self-hosted/configuration/secrets) for what each does.
:::
## Step-by-step walkthrough
@@ -45,7 +42,7 @@ This uses a **Docker Compose resource**: you paste the compose file and set ever
Log in to Coolify and **create a project**. When you add a resource, Coolify asks what type it is — GitHub, a Docker image, a service, or a **Docker Compose** file. Choose **Docker Compose**, then **paste the contents of [`docker-compose.images.yaml`](https://github.com/classroomio/classroomio/blob/main/docker-compose.images.yaml)** into the editor.
![Showing list of options to choose from](/coolify-02-services.webp)
![Showing list of options to choose from](/docs/coolify-02-services.webp)
</Step>
@@ -53,8 +50,8 @@ Log in to Coolify and **create a project**. When you add a resource, Coolify ask
### Confirm the services parsed
After saving, Coolify parses the compose file and lists the services: `postgres`, `redis`, `api`, `dashboard`, `jobs` (and `minio` once you enable its profile in the next step). Confirm they all appear before moving on.
![Select composer file type](/coolify-01-add-resource.webp)
![Coolify showing the parsed ClassroomIO services](/coolify-02-services.webp)
![Select composer file type](/docs/coolify-01-add-resource.webp)
![Coolify showing the parsed ClassroomIO services](/docs/coolify-02-services.webp)
</Step>
<Step>
@@ -79,15 +76,15 @@ SMTP_SENDER=ClassroomIO <you@yourdomain.com>
👉 The essentials above get you started, but the complete list of what to set — object storage, domains, and every optional variable — is in the [**Environment variable reference**](#environment-variable-reference) below. Copy that full block before you deploy.
<Callout title="How to read the reference: value = required, blank = optional">
There's no way to guess which variables Coolify needs, so the [Environment variable
reference](#environment-variable-reference) spells it out. The rule is simple: **every variable
shown with a value must be set** for a smooth deployment (secrets, database, domains, object
storage, and SMTP are all needed). **Every variable left blank after `=` is optional** — fill it in
only if you use that integration. Leaving an optional one blank never causes a build or boot error.
</Callout>
:::note[How to read the reference: value = required, blank = optional]
There's no way to guess which variables Coolify needs, so the [Environment variable
reference](#environment-variable-reference) spells it out. The rule is simple: **every variable
shown with a value must be set** for a smooth deployment (secrets, database, domains, object
storage, and SMTP are all needed). **Every variable left blank after `=` is optional** — fill it in
only if you use that integration. Leaving an optional one blank never causes a build or boot error.
:::
![Setting the environment variables in Coolify](/coolify-03-env-vars.webp)
![Setting the environment variables in Coolify](/docs/coolify-03-env-vars.webp)
</Step>
<Step>
@@ -95,14 +92,14 @@ SMTP_SENDER=ClassroomIO <you@yourdomain.com>
`CIO_VERSION` chooses the tag pulled for `classroomio/{api,dashboard,jobs}`. **Confirm the tag you use actually exists** at [hub.docker.com/r/classroomio](https://hub.docker.com/r/classroomio/) before deploying.
<Callout title="Pin a released version">
Set `CIO_VERSION=1.0.0` to pin an exact release. `latest` is the freshest `main` build — it moves
on every merge (rolling; may include unreleased code), while the version tags (`1.0.0`, `1.0`, `1`)
are immutable, pinned releases. Pinning an exact release makes upgrades deliberate and deploys
reproducible.
</Callout>
:::note[Pin a released version]
Set `CIO_VERSION=1.0.0` to pin an exact release. `latest` is the freshest `main` build — it moves
on every merge (rolling; may include unreleased code), while the version tags (`1.0.0`, `1.0`, `1`)
are immutable, pinned releases. Pinning an exact release makes upgrades deliberate and deploys
reproducible.
:::
![Confirming the classroomio image tags on Docker Hub](/coolify-04-image-tag.webp)
![Confirming the classroomio image tags on Docker Hub](/docs/coolify-04-image-tag.webp)
</Step>
<Step>
@@ -112,7 +109,7 @@ Open the **dashboard** service → **Settings** and set its domain, including th
The dashboard proxies API calls server-side, so you normally only expose the dashboard publicly — the `api` service stays internal.
![Setting the dashboard service domain in Coolify](/coolify-05-dashboard-domain.webp)
![Setting the dashboard service domain in Coolify](/docs/coolify-05-dashboard-domain.webp)
</Step>
<Step>
@@ -120,7 +117,7 @@ The dashboard proxies API calls server-side, so you normally only expose the das
Click **Deploy**. Coolify pulls `classroomio/{api,dashboard,jobs}:${CIO_VERSION}`, starts Postgres + Redis, the API container runs database setup on first boot, then the dashboard and the **jobs worker** come up. Watch the **logs** — they tell you when every service is up.
![Coolify deploy logs showing the stack coming up](/coolify-06-deploy-logs.webp)
![Coolify deploy logs showing the stack coming up](/docs/coolify-06-deploy-logs.webp)
</Step>
<Step>
@@ -128,7 +125,7 @@ Click **Deploy**. Coolify pulls `classroomio/{api,dashboard,jobs}:${CIO_VERSION}
Once the logs show the stack is healthy, open your dashboard URL (`https://dash.yourdomain.com`) in a new browser tab and **sign up**. The **first** account becomes the organization **admin**. Every signup or sign-in after that joins as a student — unless you invite them with a specific role.
![The running ClassroomIO dashboard sign-up page](/railway-12-signup.webp)
![The running ClassroomIO dashboard sign-up page](/docs/railway-12-signup.webp)
</Step>
</Steps>
@@ -137,11 +134,11 @@ Once the logs show the stack is healthy, open your dashboard URL (`https://dash.
You set these on the resource's Environment Variables page (Step 3). This is the complete picture — copy the block below, then replace the placeholder values with your own.
<Callout title="Value = required, blank = optional">
**Every variable shown with a value below is required** for a working deployment. **Every variable
left blank after `=` is optional** — fill it in only if you use that feature. A blank optional
variable never causes a build or boot error, so don't put junk into fields you aren't using.
</Callout>
:::note[Value = required, blank = optional]
**Every variable shown with a value below is required** for a working deployment. **Every variable
left blank after `=` is optional** — fill it in only if you use that feature. A blank optional
variable never causes a build or boot error, so don't put junk into fields you aren't using.
:::
```zsh title=".env — a complete working Coolify deployment"
# ── Secrets (generate with: openssl rand -hex 32) ──
@@ -228,12 +225,12 @@ SKIP_DB_SETUP= # blank = run DB setup on first boot (leave blank)
| `CSP_MEDIA_SRC_DOMAINS` | Your storage domain — otherwise the browser's CSP blocks uploaded media. |
| `SMTP_HOST` / `SMTP_PORT` / `SMTP_USER` / `SMTP_PASSWORD` / `SMTP_SENDER` | SMTP server for signup verification, password reset, and invites. |
<Callout type="warn" title="If the stack won't start at all">
Of the above, `BETTER_AUTH_SECRET` and `PRIVATE_SERVER_KEY` are the two the compose file
**fails fast** on: leave either empty and the containers refuse to boot (so ClassroomIO never runs
with a predictable session-signing secret). The rest won't abort the boot, but the deployment
won't work correctly without them — set them all.
</Callout>
:::warning[If the stack won't start at all]
Of the above, `BETTER_AUTH_SECRET` and `PRIVATE_SERVER_KEY` are the two the compose file
**fails fast** on: leave either empty and the containers refuse to boot (so ClassroomIO never runs
with a predictable session-signing secret). The rest won't abort the boot, but the deployment
won't work correctly without them — set them all.
:::
### Optional — leave blank unless you need them
@@ -253,31 +250,29 @@ SKIP_DB_SETUP= # blank = run DB setup on first boot (leave blank)
### Object storage
Recommended: the **bundled MinIO** — enable its profile with `COMPOSE_PROFILES=minio` and Coolify
runs MinIO alongside the stack. Give the MinIO volume persistent storage in Coolify and route its
public URL (`OBJECT_STORAGE_PUBLIC_ENDPOINT` / `OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL`) so browsers
can load media. Prefer fully-managed storage instead? Point the same `OBJECT_STORAGE_*` variables
at external **S3 / Cloudflare R2** and leave the `minio` profile off — see the
[Cloudflare Setup](/docs/contributor-guides/cloudflare-setup) guide for R2.
Enable the **bundled MinIO** with `COMPOSE_PROFILES=minio` and Coolify runs it alongside the stack.
**Give the MinIO volume persistent storage in Coolify** and route its public URL
(`OBJECT_STORAGE_PUBLIC_ENDPOINT` / `OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL`) so browsers can load media.
<Callout type="warn" title="MinIO is your durability boundary">
Single-node MinIO has no redundancy — its volume is the only thing protecting your uploads. Give
it persistent storage in Coolify and back it up. If you'd rather not operate storage, use external
**Cloudflare R2 / AWS S3** instead (R2 has no egress fees): point the same `OBJECT_STORAGE_*`
vars at the provider.
</Callout>
:::warning[MinIO is your durability boundary]
Single-node MinIO has no redundancy — its volume is the only thing protecting your uploads, so give it
persistent storage in Coolify and back it up.
:::
Prefer managed storage? Point the `OBJECT_STORAGE_*` variables at external S3 / Cloudflare R2 and leave
the `minio` profile off. See [Configuration → Object storage & uploads](/self-hosted/configuration/storage).
### Upload size limits (optional)
Set `UPLOAD_MAX_*_MB` in Coolify's env for both the `api` and `dashboard` services (same values in
each), e.g. `UPLOAD_MAX_DOCUMENT_MB=50`. See
[Docker self-hosting → Upload file size limits](/docs/self-hosted/docker#optional--upload-file-size-limits).
[Configuration → Upload file size limits](/self-hosted/configuration/storage#upload-file-size-limits).
## Upgrades & rollback
Bump `CIO_VERSION` to a newer release and redeploy; roll back by setting it to the previous version and redeploying. Because you pin a version, a new release never restarts you onto broken images unattended.
Bump `CIO_VERSION` to a newer release and redeploy; roll back by setting it to the previous version and redeploying. Because you pin a version, a new release never restarts you onto broken images unattended. See [Configuration → Versions & upgrades](/self-hosted/configuration/versions) for the full upgrade and backup steps.
<Callout type="warn" title="Back up before upgrading">
Schema migrations run automatically on API startup. Back up your Postgres (and object storage)
before a major upgrade.
</Callout>
:::warning[Back up before upgrading]
Schema migrations run automatically on API startup. Back up your Postgres (and object storage)
before a major upgrade.
:::
@@ -0,0 +1,559 @@
---
title: DigitalOcean
description: Deploy ClassroomIO to a DigitalOcean droplet with Docker Compose and Caddy, including automatic HTTPS.
---
Run ClassroomIO on a plain **DigitalOcean droplet** — no PaaS in between. You get the
[pull-based Docker Compose stack](/self-hosted/docker) plus a [Caddy](https://caddyserver.com)
reverse proxy that obtains and renews HTTPS certificates automatically.
Nothing here is DigitalOcean-specific. The same steps work unchanged on **Hetzner, Vultr, Linode,
AWS Lightsail**, or any Ubuntu 24.04 server you can SSH into.
:::note[How this differs from the PaaS guides]
[Railway](/self-hosted/railway), [Coolify](/self-hosted/coolify) and [Dokploy](/self-hosted/dokploy)
terminate HTTPS for you. On a raw VPS you provide that yourself — this page covers the reverse proxy,
the certificates, and closing your internal ports off from the internet.
:::
## Prerequisites
- A **droplet running Ubuntu 24.04**. Minimum **2 vCPU / 4 GB RAM / 80 GB disk**; see
[Server requirements](/self-hosted#server-requirements). Measured idle usage of the full stack is
~1.4 GB, but the jobs worker spikes during video transcoding.
- **SSH access** to it.
- A **domain** you control, with two DNS **A records** pointing at the droplet's public IP:
| Type | Name | Value |
| ---- | ----- | ----------------- |
| A | `app` | your droplet's IP |
| A | `s3` | your droplet's IP |
- An **SMTP provider** for signup verification, password resets, and invites — see
[Configuration → Email (SMTP)](/self-hosted/configuration/email).
:::note[No domain? You can still get real HTTPS]
Use [sslip.io](https://sslip.io), a public wildcard-DNS service where `<anything>.<ip>.sslip.io`
resolves to that IP — so no records are needed. In Step 1 set
`export DOMAIN=<your-droplet-ip>.sslip.io` instead of your own domain, and **every other command on
this page is identical**. Read the [caveat](#a-note-on-sslipio) before using it for anything real.
:::
## Why two hostnames
The dashboard is served from `app.<your-domain>`, and object storage from `s3.<your-domain>`.
Storage needs its own hostname because video and document uploads hand **presigned S3 URLs** to the
browser, and AWS SigV4 signs the full request path. Serving storage under a path on the app host
(`app.example.com/s3`) means the proxy must strip that prefix, which invalidates the signature and
breaks every upload with `SignatureDoesNotMatch`. A separate hostname preserves the path and the
`Host` header.
## Deployment
<Steps>
<Step>
### Set your domain
Everything below derives from this one variable. Run it in **every** new SSH session:
```zsh title="Terminal"
export DOMAIN=yourdomain.com
```
Using sslip.io instead? Set it to your droplet's IP plus `.sslip.io`:
```zsh title="Terminal"
export DOMAIN=203.0.113.10.sslip.io
```
Your dashboard will be `https://app.$DOMAIN` and storage `https://s3.$DOMAIN`.
</Step>
<Step>
### Create a non-root user
Connect as root, then create the account you'll actually work from:
```zsh title="Terminal"
adduser cio
usermod -aG sudo cio
rsync --archive --chown=cio:cio ~/.ssh /home/cio
```
**Before going further**, open a second terminal and confirm the new user can log in:
```zsh title="Terminal"
ssh cio@<your-droplet-ip> 'echo works'
```
:::danger[Verify first, then lock down]
Only once that prints `works`, disable root login. If the key copy failed and you have already
disabled root SSH, you are locked out of the droplet.
```zsh title="Terminal"
sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
systemctl restart ssh
```
:::
Close the root session. Everything from here runs as `cio`.
</Step>
<Step>
### Firewall and swap
```zsh title="Terminal"
sudo apt update && sudo apt upgrade -y
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
sudo apt install -y unattended-upgrades
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
```
The swap file matters: a video transcode can spike memory enough to get Postgres killed on a 4 GB
server.
</Step>
<Step>
### Install Docker
```zsh title="Terminal"
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker cio
```
:::warning[Use the literal username, not `$USER`]
`$USER` expands to whoever runs the shell — run it from a root session and it grants **root** rather
than your user, and every later Docker command fails with `permission denied` on
`/var/run/docker.sock`. Check with `getent group docker`.
:::
Group membership only applies to a **new login**, so reconnect, then verify:
```zsh title="Terminal"
exit
ssh cio@<your-droplet-ip>
export DOMAIN=yourdomain.com
id | grep -o docker # -> docker
docker compose version # must be v2.24.0 or newer
```
Compose **v2.24.0+** is required — Step 6 uses the `!override` YAML tag. Older versions
*concatenate* port lists instead of replacing them, which would leave your internal services exposed
to the internet.
</Step>
<Step>
### Download the compose file
```zsh title="Terminal"
mkdir -p ~/classroomio && cd ~/classroomio
curl -O https://raw.githubusercontent.com/classroomio/classroomio/main/docker-compose.images.yaml
```
This is the same [pull-based stack](/self-hosted/docker#run-from-pre-built-images-recommended) used
by the other guides — it pulls the published `classroomio/*` images and builds nothing.
</Step>
<Step>
### Create the `.env` file
This generates every secret for you. Paste it whole:
```zsh title="Terminal"
cd ~/classroomio
MINIO_PASS=$(openssl rand -hex 24)
cat > .env <<EOF
CIO_VERSION=1.0.0
POSTGRES_DB=classroomio
POSTGRES_USER=postgres
POSTGRES_PASSWORD=$(openssl rand -hex 16)
BETTER_AUTH_SECRET=$(openssl rand -hex 32)
PRIVATE_SERVER_KEY=$(openssl rand -hex 32)
PUBLIC_IS_SELFHOSTED=true
PRIVATE_SERVER_URL=https://app.$DOMAIN
DASHBOARD_ORIGIN=https://app.$DOMAIN
PRIVATE_APP_HOST=$DOMAIN
PRIVATE_APP_SUBDOMAINS=app
PUBLIC_SERVER_URL=
TRUSTED_ORIGINS=
MINIO_ROOT_USER=cio-storage-admin
MINIO_ROOT_PASSWORD=$MINIO_PASS
OBJECT_STORAGE_ENDPOINT=http://minio:9000
OBJECT_STORAGE_PUBLIC_ENDPOINT=https://s3.$DOMAIN
OBJECT_STORAGE_ACCESS_KEY_ID=cio-storage-admin
OBJECT_STORAGE_SECRET_ACCESS_KEY=$MINIO_PASS
OBJECT_STORAGE_FORCE_PATH_STYLE=true
OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL=https://s3.$DOMAIN/media
CSP_MEDIA_SRC_DOMAINS=s3.$DOMAIN
CSP_CONNECT_SRC_DOMAINS=s3.$DOMAIN
SMTP_HOST=smtp.yourprovider.com
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
SMTP_SENDER=ClassroomIO <you@yourdomain.com>
OPENAI_API_KEY=
UNSPLASH_API_KEY=
LICENSE_KEY=
EOF
chmod 600 .env
```
Then fill in your SMTP credentials with `nano .env`.
Three of these break deployments more often than all the others combined:
- **`OBJECT_STORAGE_PUBLIC_ENDPOINT`** — where the browser fetches video from. Left at the template
default of `localhost`, every video URL points at the *visitor's own machine*.
- **`DASHBOARD_ORIGIN`** — must match the browser URL exactly, `https://` included, no trailing
slash. A mismatch breaks login with CSRF errors.
- **`CSP_MEDIA_SRC_DOMAINS` / `CSP_CONNECT_SRC_DOMAINS`** — because storage is on a second hostname,
the browser blocks its media unless these allow it. Symptom: the page looks fine, images and video
silently don't load, and the only clue is in the browser console.
:::note[Keep values unquoted and comment-free]
One value per line, no trailing `# comment`. Compose can fold an inline comment into the value and
silently corrupt a password or URL.
:::
Every remaining variable is documented in [Configuration](/self-hosted/configuration).
</Step>
<Step>
### Add Caddy and close the internal ports
Right now each part of ClassroomIO runs on its own port number — the dashboard on `3082`, file
storage on `9000`. That leaves two problems: nobody wants to visit `yoursite.com:3082`, and neither
port is encrypted, so there would be no padlock in the browser.
Caddy fixes both. It's a small web server that sits in front of everything on the normal web ports
(`80` and `443`). It gets an HTTPS certificate for your domain automatically, then sends each
visitor to the right service based on the address they typed:
| Visitor types | Caddy sends them to |
| ------------- | ------------------- |
| `app.<your-domain>` | the dashboard |
| `s3.<your-domain>` | file storage |
The second half of this step closes the internal ports, so the only way in from the internet is
through Caddy.
```zsh title="Terminal"
cat > ~/classroomio/docker-compose.caddy.yaml <<'EOF'
services:
caddy:
image: caddy:2-alpine
container_name: cio-caddy
restart: unless-stopped
ports:
- '80:80'
- '443:443'
- '443:443/udp'
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
depends_on:
- dashboard
api:
ports: !override
- '127.0.0.1:3081:3081'
dashboard:
ports: !override
- '127.0.0.1:3082:3082'
minio:
ports: !override
- '127.0.0.1:9000:9000'
- '127.0.0.1:9001:9001'
volumes:
caddy-data:
caddy-config:
EOF
```
```zsh title="Terminal"
cat > ~/classroomio/Caddyfile <<EOF
app.$DOMAIN {
encode gzip
reverse_proxy dashboard:3082
}
s3.$DOMAIN {
reverse_proxy minio:9000
}
EOF
```
Those eight lines are the entire HTTPS setup. The first time someone visits your site, Caddy asks
Let's Encrypt for a free certificate, installs it, and from then on renews it before it expires —
you never touch it again.
You could use nginx instead, but you'd also be installing certbot, setting up a renewal timer, and
hand-writing the forwarding rules that Caddy applies by default.
:::warning[Your firewall does not cover Docker]
You set up a firewall back in Step 3. It does **not** apply to Docker containers — Docker writes its
own networking rules underneath it, so a container that publishes a port stays reachable from the
internet even when `ufw` says that port is blocked.
That's why the `!override` lines above matter. `127.0.0.1` means "this machine only", so those
services stop accepting outside connections altogether. Caddy still reaches them because it sits on
the same internal Docker network. Step 9 proves it from outside the server.
:::
</Step>
<Step>
### Download the images
Save the long compose command as an alias first:
```zsh title="Terminal"
echo "alias dc='docker compose -f ~/classroomio/docker-compose.images.yaml -f ~/classroomio/docker-compose.caddy.yaml --env-file ~/classroomio/.env -p classroomio --profile minio'" >> ~/.bashrc
source ~/.bashrc
dc config --services
```
You should see: `postgres redis api dashboard jobs minio minio-init caddy`
Then pull:
```zsh title="Terminal"
dc pull
```
:::warning[This downloads ~12 GB and takes 10–20 minutes]
The images are large (`api` 3.7 GB, `dashboard` 3.9 GB, `jobs` 4.2 GB). On a 2-vCPU droplet the
**progress bars sit still for minutes at a time — this is normal, it is not frozen.** Don't cancel it.
:::
</Step>
<Step>
### Start and verify
```zsh title="Terminal"
dc up -d
```
Services start in dependency order — Postgres and Redis must report healthy before the API starts,
because the API runs database migrations on boot. A full start takes about **40 seconds**.
```zsh title="Terminal"
dc ps
```
```
NAME STATUS PORTS
cio-api Up About a minute (healthy) 127.0.0.1:3081->3081/tcp
cio-caddy Up 42 seconds 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp, ...
cio-dashboard Up 43 seconds 3000/tcp, 127.0.0.1:3082->3082/tcp
cio-jobs Up About a minute
cio-minio Up About a minute 127.0.0.1:9000-9001->9000-9001/tcp
cio-postgres Up About a minute (healthy)
cio-redis Up About a minute (healthy)
```
`cio-minio-init` is absent on purpose — it creates the storage buckets and exits. Confirm it worked:
```zsh title="Terminal"
docker logs cio-minio-init | tail -4
```
```
Bucket created successfully `local/videos`.
Bucket created successfully `local/documents`.
Bucket created successfully `local/media`.
Access permission for `local/media` is set to `download`
```
Check the certificates were issued:
```zsh title="Terminal"
docker logs cio-caddy 2>&1 | grep "certificate obtained"
```
Now confirm nothing internal is exposed. **Run this from your own machine, not the droplet:**
```zsh title="Terminal"
for p in 3081 3082 9000 9001; do
curl -s --max-time 6 -o /dev/null "http://<your-droplet-ip>:$p" \
&& echo "port $p REACHABLE <-- problem" \
|| echo "port $p no response (correct)"
done
```
All four must report `no response`. If any is reachable, your Compose version ignored `!override` —
check that `docker compose version` is v2.24.0 or newer.
</Step>
<Step>
### Sign up
Open `https://app.<your-domain>` and create the first account — it becomes the organization
**admin**. Every signup after that joins as a student unless you invite them with a specific role.
Then verify uploads end to end: create a course, upload an image, and upload a short video. Watch the
worker process it with `dc logs -f jobs`. In your browser's DevTools the media URLs must point at
`https://s3.<your-domain>/...` — if you see `localhost:9000`, `OBJECT_STORAGE_PUBLIC_ENDPOINT` is
wrong.
</Step>
</Steps>
## Day-to-day commands
```zsh title="Terminal"
dc ps # status
dc logs -f api dashboard # stream logs
dc up -d # apply an .env change
dc down # stop, keeping all data
```
:::danger[Never use `dc down -v`]
The `-v` flag deletes the `postgres-data`, `redis-data` and `minio-data` volumes — your database and
every upload. Plain `dc down` is always safe.
:::
## Backups
```zsh title="Terminal"
mkdir -p ~/backups
docker exec cio-postgres pg_dump -U postgres classroomio | gzip > ~/backups/cio-db-$(date +%F).sql.gz
docker run --rm -v classroomio_minio-data:/data -v ~/backups:/backup alpine \
tar czf /backup/cio-minio-$(date +%F).tar.gz -C /data .
```
Run both on a daily cron, copy them off the droplet, and enable DigitalOcean snapshots for whole-box
recovery.
## A note on sslip.io
sslip.io is genuinely useful for evaluation, but it is **not on the
[Public Suffix List](https://publicsuffix.org)**. Let's Encrypt scopes its *certificates per
registered domain* limit using that list, so certificates for `sslip.io` count against a quota shared
with everyone else using the service.
In practice that means issuance can occasionally fail with `too many certificates already issued`
through no fault of your configuration. If it happens, wait a few hours or move to a domain of your
own. **For production, use your own domain.**
## Moving from sslip.io to your own domain
1. Add the two **A records** from [Prerequisites](#prerequisites) and confirm with
`dig +short app.yourdomain.com`.
2. Update six lines in `~/classroomio/.env`:
```zsh title=".env"
DASHBOARD_ORIGIN=https://app.yourdomain.com
PRIVATE_APP_HOST=yourdomain.com
OBJECT_STORAGE_PUBLIC_ENDPOINT=https://s3.yourdomain.com
OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL=https://s3.yourdomain.com/media
CSP_MEDIA_SRC_DOMAINS=s3.yourdomain.com
CSP_CONNECT_SRC_DOMAINS=s3.yourdomain.com
```
3. Change the two site addresses in `~/classroomio/Caddyfile`.
4. Apply, then clear cached media URLs — presigned URLs are cached in Redis for 50 minutes, so
without this the old hostname keeps being served:
```zsh title="Terminal"
dc up -d
docker exec cio-redis sh -c "redis-cli --scan --pattern 'presign:download:*' | xargs -r redis-cli del"
```
Existing uploads keep working — media URLs are generated per request from the new endpoint.
## Troubleshooting
| Symptom | Cause |
| ------- | ----- |
| `permission denied` on `/var/run/docker.sock` | Your user isn't in the `docker` group, or you haven't logged out and back in since adding it |
| A port answers from outside the droplet | Compose older than v2.24.0 ignored `!override` |
| No certificate issued | Port 80 must be reachable; behind Cloudflare set the records to **DNS only** (the orange proxy breaks HTTP-01 validation) |
| Video won't play, or `SignatureDoesNotMatch` | `OBJECT_STORAGE_PUBLIC_ENDPOINT` must be a bare hostname, never a path prefix |
| Images/video blank, CSP errors in console | `CSP_MEDIA_SRC_DOMAINS` / `CSP_CONNECT_SRC_DOMAINS` missing |
| Uploads stuck at "processing" | The `jobs` worker isn't running — check `dc logs jobs` |
| No emails | `dc logs api \| grep -i smtp`; `ECONNREFUSED` means SMTP isn't configured |
| Login loops or CSRF errors | `DASHBOARD_ORIGIN` doesn't exactly match the browser URL |
| Containers being OOM-killed | A transcode on 4 GB can exhaust memory; confirm swap with `swapon --show`, or resize to 8 GB |
## Configuration reference
This deployment reads the same variables as every other self-hosted install:
<CardGroup cols={2}>
<Card title="Environment & secrets" href="/self-hosted/configuration/secrets" icon="key-round">
`BETTER_AUTH_SECRET`, `PRIVATE_SERVER_KEY`, database, mode/routing.
</Card>
<Card title="Email (SMTP)" href="/self-hosted/configuration/email" icon="mail">
Signup verification, password resets, invites.
</Card>
<Card title="Object storage & uploads" href="/self-hosted/configuration/storage" icon="hard-drive">
MinIO or external S3/R2, plus upload size limits.
</Card>
<Card title="CSP / allowed domains" href="/self-hosted/configuration/csp" icon="shield">
Allowed external CDN, font, and media domains.
</Card>
<Card title="Integrations" href="/self-hosted/configuration/integrations" icon="plug">
Google OAuth, Unsplash, OpenAI.
</Card>
<Card title="Versions & upgrades" href="/self-hosted/configuration/versions" icon="tag">
Pinning `CIO_VERSION`, upgrading, and rolling back.
</Card>
</CardGroup>
## Upgrades
Check which versions exist, then pin one — never guess a tag, as an unpublished one fails the pull
with `manifest unknown`:
```zsh title="Terminal"
curl -s "https://hub.docker.com/v2/repositories/classroomio/dashboard/tags?page_size=20&ordering=last_updated" \
| tr ',' '\n' | grep '"name"'
sed -i 's/^CIO_VERSION=.*/CIO_VERSION=<exact version>/' ~/classroomio/.env
dc pull && dc up -d
```
Migrations run automatically on API startup. Roll back by setting `CIO_VERSION` to the previous
release and repeating — which is why you pin an exact version rather than `latest`. See
[Configuration → Versions & upgrades](/self-hosted/configuration/versions).
+47 -127
View File
@@ -3,14 +3,12 @@ title: Docker
description: Self-host ClassroomIO with Docker — pull pre-built images (recommended) or build from source.
---
import { Callout } from 'fumadocs-ui/components/callout';
Docker is one way to self-host ClassroomIO. There are two paths:
- **Run from pre-built images (recommended)** — pull the published `classroomio/{api,dashboard,jobs}` images. You only need one compose file and a `.env`; no repository clone and no heavy local build.
- **Build from source** — clone the repo and build the images yourself (needs ~8 GB RAM for the dashboard build).
![Selfhost ClassroomIO](/selfhost-classroomio.webp)
![Selfhost ClassroomIO](/docs/selfhost-classroomio.webp)
## Run from Pre-built Images (recommended)
@@ -43,12 +41,11 @@ SMTP_PASSWORD=
SMTP_SENDER=
```
<Callout type="error" title="Set strong secrets — the stack won't start without them">
This path does **not** run `run-docker-full-stack.sh`, so secrets are not auto-generated. You must
set `BETTER_AUTH_SECRET` and `PRIVATE_SERVER_KEY` yourself — e.g. `openssl rand -hex 32`. The
compose file fails fast with a clear error if either is empty, so it never boots with a
predictable session-signing secret.
</Callout>
:::danger[Set strong secrets — the stack won't start without them]
This path does **not** run `run-docker-full-stack.sh`, so secrets are not auto-generated. You must set
`BETTER_AUTH_SECRET` and `PRIVATE_SERVER_KEY` yourself, or the compose file fails fast. See
[Configuration → Environment & secrets](/self-hosted/configuration/secrets).
:::
3. Pull and start the stack (MinIO is included via the `minio` profile):
@@ -103,131 +100,54 @@ Open `.env` and set `DASHBOARD_ORIGIN` for your domain. For local development th
The script builds and starts Postgres, Redis, MinIO (object storage), the API, the dashboard, and the background **jobs** worker.
<Callout title="Default is pull, not build">
Run without `--build` and the script **pulls** the published `classroomio/*` images at the
`CIO_VERSION` in your `.env` instead of building locally — that's the faster default path.
</Callout>
:::note[Default is pull, not build]
Run without `--build` and the script **pulls** the published `classroomio/*` images at the
`CIO_VERSION` in your `.env` instead of building locally — that's the faster default path.
:::
## Upgrading to a New Version
## One `.env`, One Command
ClassroomIO publishes versioned images. `CIO_VERSION` controls which tag you run:
Docker self-hosting uses a single root `.env` file. Every `docker compose` command reads it via `--env-file .env` — you never need separate env files per service. See [`.env.example`](https://github.com/classroomio/classroomio/blob/main/.env.example) for the full template.
- Pin an exact release (e.g. `1.0.0`) in production so a merge to `main` never restarts you onto changed images unattended.
- `latest` = the freshest `main` build — it moves on every merge (rolling; may include unreleased code). Version tags (`1.0.0`, `1.0`, `1`) are the immutable, pinned releases.
## Configuration
**Pre-built images path** — bump `CIO_VERSION` in `.env`, then re-pull and recreate:
Every variable the stack reads is documented once in the shared
[Configuration](/self-hosted/configuration) section — set the ones you need in your root `.env`:
<CardGroup cols={2}>
<Card title="Environment & secrets" href="/self-hosted/configuration/secrets" icon="key-round">
`BETTER_AUTH_SECRET`, `PRIVATE_SERVER_KEY`, database, and mode/routing basics.
</Card>
<Card title="Email (SMTP)" href="/self-hosted/configuration/email" icon="mail">
SMTP for signup verification, password resets, and invites.
</Card>
<Card title="Object storage & uploads" href="/self-hosted/configuration/storage" icon="hard-drive">
MinIO or external S3/R2, plus upload size limits.
</Card>
<Card title="CSP / allowed domains" href="/self-hosted/configuration/csp" icon="shield">
Allowed external CDN, font, and media domains.
</Card>
<Card title="Integrations" href="/self-hosted/configuration/integrations" icon="plug">
Google OAuth, Unsplash, OpenAI, branding.
</Card>
<Card title="SSO & licensing" href="/self-hosted/configuration/sso" icon="id-card">
`LICENSE_KEY` for Enterprise features.
</Card>
</CardGroup>
## Upgrading
ClassroomIO publishes versioned images and `CIO_VERSION` controls which tag you run. To upgrade the
pre-built images path, bump `CIO_VERSION` and re-pull:
```zsh title="Terminal"
docker compose -f docker-compose.images.yaml --env-file .env --profile minio pull
docker compose -f docker-compose.images.yaml --env-file .env --profile minio up -d
```
**Rollback** — set `CIO_VERSION` back to the previous release and run the same two commands.
**Build-from-source path** — `git pull`, then re-run `./run-docker-full-stack.sh --build` to rebuild.
<Callout type="warn" title="Back up before upgrading">
Database schema migrations run automatically when the API starts. Back up your Postgres volume
(and object storage) before a major upgrade, and avoid `docker compose down -v` / `docker volume
prune`, which delete your data volumes.
</Callout>
## One `.env`, One Command
Docker self-hosting uses a single root `.env` file. Every `docker compose` command reads it via `--env-file .env` — you never need separate env files per service. See [`.env.example`](https://github.com/classroomio/classroomio/blob/main/.env.example) for the full template.
## Environment Variables
### Required
| Variable | Description | Default |
|----------|-------------|---------|
| `POSTGRES_DB` | Database name | `classroomio` |
| `POSTGRES_USER` | Database user | `postgres` |
| `POSTGRES_PASSWORD` | Database password | `postgres` |
| `BETTER_AUTH_SECRET` | Auth session signing secret (required — stack won't start if empty) | — |
| `PRIVATE_SERVER_KEY` | Dashboard-to-API auth key (must be identical in api + dashboard) | Auto-generated by the script |
| `PUBLIC_IS_SELFHOSTED` | Self-hosted mode flag | `true` |
| `PRIVATE_SERVER_URL` | Internal API URL for SSR (Docker network) | `http://api:3081` |
| `DASHBOARD_ORIGIN` | Dashboard origin for invite links, email links, etc. | `http://localhost:3082` |
### CSP Domains (Runtime)
CSP domain variables are **runtime** — set them in your `.env`. No image rebuild is needed when changing them.
| Variable | Description |
|----------|-------------|
| `ALLOWED_EXTERNAL_DOMAINS` | Overrides all per-directive CSP vars (comma-separated) |
| `CSP_SCRIPT_SRC_DOMAINS` | Domains allowed in `script-src` |
| `CSP_STYLE_SRC_DOMAINS` | Domains allowed in `style-src` |
| `CSP_CONNECT_SRC_DOMAINS` | Domains allowed in `connect-src` |
| `CSP_FRAME_SRC_DOMAINS` | Domains allowed in `frame-src` |
| `CSP_FONT_SRC_DOMAINS` | Domains allowed in `font-src` |
| `CSP_MEDIA_SRC_DOMAINS` | Domains allowed in `img-src` and `media-src` |
<Callout type="warn" title="CSP Domains">
When `PUBLIC_IS_SELFHOSTED=true`, the dashboard starts with only `'self'` allowed. Set `ALLOWED_EXTERNAL_DOMAINS` (or the per-directive vars) with your CDN, video, and font domains. The API does not need to be added for normal dashboard traffic because requests are proxied same-origin. Since these are runtime env vars, you can change them by restarting the container — no rebuild required.
</Callout>
### Optional — Email
Configure SMTP. Without email config, the app works but cannot send invites, password resets, or notifications.
| Variable | Description |
|----------|-------------|
| `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASSWORD`, `SMTP_SENDER` | SMTP server |
### Optional — Object Storage
The bundled **MinIO** is included by default and the startup script auto-configures it — you only need to change these for custom credentials or an external S3-compatible service.
| Variable | Default |
|----------|---------|
| `MINIO_ROOT_USER` | randomized by the script (`minioadmin` if you skip it) |
| `MINIO_ROOT_PASSWORD` | randomized by the script (`minioadmin` if you skip it) |
| `OBJECT_STORAGE_ENDPOINT` | `http://minio:9000` |
| `OBJECT_STORAGE_PUBLIC_ENDPOINT` | `http://localhost:9000` |
| `OBJECT_STORAGE_ACCESS_KEY_ID` | matches `MINIO_ROOT_USER` |
| `OBJECT_STORAGE_SECRET_ACCESS_KEY` | matches `MINIO_ROOT_PASSWORD` |
| `OBJECT_STORAGE_FORCE_PATH_STYLE` | `true` |
| `OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL` | `http://localhost:9000/media` |
<Callout title="Using AWS S3 or Cloudflare R2 instead of MinIO">
The `OBJECT_STORAGE_*` variables speak the S3 API, so they work with any S3-compatible provider.
To use Cloudflare R2 or AWS S3, point `OBJECT_STORAGE_ENDPOINT` at the provider's endpoint and set
the matching access key / secret, then start without the bundled MinIO via
`./run-docker-full-stack.sh --no-minio`. See the [Cloudflare Setup](/docs/contributor-guides/cloudflare-setup) guide for R2.
</Callout>
### Optional — Integrations
| Variable | Description |
|----------|-------------|
| `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` | Google OAuth |
| `PRIVATE_APP_HOST`, `PRIVATE_APP_SUBDOMAINS` | Multi-subdomain routing |
| `UNSPLASH_API_KEY` | Unsplash image search |
| `OPENAI_API_KEY` | Transcription + AI course generation (used by the jobs worker) |
| `LICENSE_KEY` | Enterprise features (SSO, token-auth, no-tracking) |
| `PUBLIC_APP_TITLE`, `PUBLIC_APP_DESCRIPTION`, `PUBLIC_OG_IMAGE_URL` | Custom branding |
### Optional — Upload file size limits
Set these in the root `.env` to raise or lower upload caps (values are in **megabytes**). Compose passes them through to the `api` and `dashboard` containers. Unset = built-in defaults. On [Railway](/docs/self-hosted/railway) or [Coolify](/docs/self-hosted/coolify), set the same variables on both the API and dashboard services.
| Variable | Default | Applies to |
|----------|---------|------------|
| `UPLOAD_MAX_DOCUMENT_MB` | 5 | Course lesson documents |
| `UPLOAD_MAX_IMAGE_MB` | 5 | Avatars, media library, editor images |
| `UPLOAD_MAX_VIDEO_MB` | 800 | Lesson video uploads |
| `UPLOAD_MAX_EXERCISE_FILE_MB` | 2 | Student assignment file-upload questions |
| `UPLOAD_MAX_AGENT_DOCUMENT_MB` | 5 | AI assistant document uploads |
| `UPLOAD_MAX_LANDING_IMAGE_MB` | 0.5 | Org landing page cover images |
| `UPLOAD_MAX_THUMBNAIL_MB` | 5 | Media thumbnail uploads |
<Callout type="warn" title="Reverse proxy limits">
If you raise these values, also increase your reverse proxy body-size limit (e.g. nginx
`client_max_body_size`) so large uploads are not rejected before they reach ClassroomIO.
</Callout>
For the build-from-source path, `git pull` and re-run `./run-docker-full-stack.sh --build`. See
[Configuration → Versions & upgrades](/self-hosted/configuration/versions) for pinning, rollback, and
the pre-upgrade backup steps.
## Common Commands
@@ -253,8 +173,8 @@ docker image prune -a -f # free unused images
docker container prune -f # free stopped containers
```
<Callout type="warn" title="Volume prune deletes data">
Avoid `docker volume prune` after `docker compose ... down` — it can remove your database and storage volumes.
</Callout>
:::warning[Volume prune deletes data]
Avoid `docker volume prune` after `docker compose ... down` — it can remove your database and storage volumes.
:::
Then restart: `./run-docker-full-stack.sh`
@@ -0,0 +1,194 @@
---
title: Dokploy
description: Deploy ClassroomIO on Dokploy using the pull-based Docker Compose stack.
---
[Dokploy](https://dokploy.com) is an open-source, self-hosted PaaS — a Heroku/Vercel alternative you run
on your own VPS. It deploys ClassroomIO from the published `classroomio/*` images using the **same
pull-based Docker Compose file** as the [Docker](/self-hosted/docker) and [Coolify](/self-hosted/coolify)
guides — Dokploy **pulls** the images and never builds anything.
## Watch the walkthrough
Prefer to follow along? This video walks through the whole Dokploy deployment end to end. The written
steps below mirror it.
<YouTube id="CJIZhdvxXIA" />
## Prerequisites
- A running **Dokploy instance** with a **server connected** to it. Dokploy runs on any VPS —
DigitalOcean, Hetzner, AWS (Lightsail/EC2), or another provider. See Dokploy's
[installation guide](https://docs.dokploy.com/docs/core/installation) to set this up.
- A server sized for ClassroomIO. Dokploy's own baseline stack needs ~2 GB RAM, and ClassroomIO's
seven services need **8 GB for reliable operation** — provision for both. See
[Server requirements](/self-hosted#server-requirements).
- A **domain** with a DNS **A record pointing at your server's public IP**. Dokploy's built-in Traefik
proxy terminates HTTPS and issues the certificate.
- An **SMTP provider** for signup verification, password reset, and invite emails — see
[Configuration → Email (SMTP)](/self-hosted/configuration/email).
- *Optional:* an **OpenAI API key** (transcription + AI course generation) and Google OAuth / Unsplash
keys — see [Configuration → Integrations](/self-hosted/configuration/integrations).
:::note[Two secrets to generate first]
Dokploy does **not** run our secret-generation script, so generate `BETTER_AUTH_SECRET` and
`PRIVATE_SERVER_KEY` yourself and keep them handy:
```zsh title="Terminal"
openssl rand -hex 32 # BETTER_AUTH_SECRET
openssl rand -hex 32 # PRIVATE_SERVER_KEY (must be identical in the api and dashboard services)
```
See [Configuration → Environment & secrets](/self-hosted/configuration/secrets) for what each does.
:::
## Step-by-step walkthrough
This uses a Dokploy **Compose** service: you paste the compose file, set every environment value
yourself, deploy, then attach the dashboard domain. The flow is: **create a project → add a Compose
service → paste the compose file → set environment variables → point your domain → deploy → add the
dashboard domain → sign up**.
<Steps>
<Step>
### Create a project
In the Dokploy dashboard, open **Projects** and click **Create Project**. Give it a name (e.g.
`classroomio`) and open it.
![Creating a project in Dokploy](/docs/dokploy-01-create-project.webp)
</Step>
<Step>
### Add a Compose service
Inside the project, click **Create Service** and choose **Compose** from the service types. When
prompted, **select the server** you connected to Dokploy — this is the VPS ClassroomIO runs on.
![Adding a Compose service and selecting the server](/docs/dokploy-02-compose-service.webp)
</Step>
<Step>
### Choose the Raw (manual) provider and paste the compose file
Dokploy asks where the compose file comes from — GitHub, GitLab, Bitbucket, or **Raw**. Choose **Raw**
(the manual option) and paste the contents of
[`docker-compose.images.yaml`](https://github.com/classroomio/classroomio/blob/main/docker-compose.images.yaml)
— the same pull-based stack used for the [Docker](/self-hosted/docker) and [Coolify](/self-hosted/coolify)
deployments.
![Choosing the Raw provider and pasting the compose file](/docs/dokploy-03-raw-compose.webp)
</Step>
<Step>
### Set the environment variables
Open the service's **Environment** tab and add your configuration. At minimum:
```zsh title="Environment"
COMPOSE_PROFILES=minio # runs the bundled MinIO object storage
DASHBOARD_ORIGIN=https://dash.yourdomain.com
PUBLIC_SERVER_URL=https://dash.yourdomain.com
BETTER_AUTH_SECRET=<openssl rand -hex 32>
PRIVATE_SERVER_KEY=<openssl rand -hex 32> # identical in the api and dashboard services
POSTGRES_PASSWORD=<strong password>
# Email (signup verification, password reset, invites)
SMTP_HOST=smtp.yourprovider.com
SMTP_PORT=587
SMTP_USER=<smtp username>
SMTP_PASSWORD=<smtp password>
SMTP_SENDER=ClassroomIO <you@yourdomain.com>
```
The full variable reference — object storage, CSP, integrations, licensing, versions — lives in the
shared [Configuration](/self-hosted/configuration) section. Copy what you need before deploying.
![Setting the environment variables in Dokploy](/docs/dokploy-04-env-vars.webp)
</Step>
<Step>
### Point your domain at the server
Before the domain will work, add a DNS **A record** for it (e.g. `dash.yourdomain.com`) pointing at
your server's public IP. Do this for the dashboard domain — and any other domain you plan to use (such
as storage). DNS can take a few minutes to propagate.
</Step>
<Step>
### Deploy
Click **Deploy**. Dokploy pulls `classroomio/{api,dashboard,jobs}` plus Postgres, Redis, and MinIO,
then starts the stack. The API runs database setup on first boot. Watch the **logs** until every
service is up — this is your first deploy.
![Dokploy deploy logs showing the stack coming up](/docs/dokploy-05-deploy-logs.webp)
</Step>
<Step>
### Add the dashboard domain
Open the service's **Domains** section and click **Add Domain**. In the modal that appears:
- **Service:** select **dashboard** — the service this domain routes to.
- **Host:** your dashboard domain, e.g. `dash.yourdomain.com`.
- **Container Port:** `3082` — the port the dashboard listens on.
- **HTTPS:** turn it on, then choose **Let's Encrypt** as the certificate provider.
Create the domain. Dokploy provisions the certificate through Traefik — it usually only takes a moment.
![Adding the dashboard domain with port 3082 and Let's Encrypt](/docs/dokploy-06-add-domain.webp)
</Step>
<Step>
### Open the app and sign up
Once the certificate is issued, open your dashboard URL (`https://dash.yourdomain.com`) in a new browser
tab and **sign up**. The **first** account becomes the organization **admin**; every signup after that
joins as a student unless you invite them with a specific role.
![The running ClassroomIO dashboard sign-up page](/docs/dokploy-07-signup.webp)
</Step>
</Steps>
## Environment variable reference
Dokploy reads the same variables as every other self-hosted install. Rather than repeat them, use the
shared [Configuration](/self-hosted/configuration) section:
<CardGroup cols={2}>
<Card title="Environment & secrets" href="/self-hosted/configuration/secrets" icon="key-round">
`BETTER_AUTH_SECRET`, `PRIVATE_SERVER_KEY`, database, mode/routing.
</Card>
<Card title="Email (SMTP)" href="/self-hosted/configuration/email" icon="mail">
Signup verification, password resets, invites.
</Card>
<Card title="Object storage & uploads" href="/self-hosted/configuration/storage" icon="hard-drive">
MinIO or external S3/R2, plus upload size limits.
</Card>
<Card title="CSP / allowed domains" href="/self-hosted/configuration/csp" icon="shield">
Allowed external CDN, font, and media domains.
</Card>
<Card title="Integrations" href="/self-hosted/configuration/integrations" icon="plug">
Google OAuth, Unsplash, OpenAI.
</Card>
<Card title="SSO & licensing" href="/self-hosted/configuration/sso" icon="id-card">
`LICENSE_KEY` for Enterprise features.
</Card>
</CardGroup>
:::warning[MinIO is your durability boundary]
If you use the bundled MinIO, give its volume **persistent storage** in Dokploy and set
`OBJECT_STORAGE_PUBLIC_ENDPOINT` / `OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL` to a domain that resolves to
your server, so browsers can load uploaded media. Prefer managed storage? Point the `OBJECT_STORAGE_*`
vars at Cloudflare R2 / AWS S3 instead. See [Object storage & uploads](/self-hosted/configuration/storage).
:::
## Upgrades & rollback
Set `CIO_VERSION` in the service environment to pin a release, then redeploy to upgrade; roll back by
setting it to the previous version and redeploying. See
[Configuration → Versions & upgrades](/self-hosted/configuration/versions) for pinning, rollback, and
the pre-upgrade backup steps.
@@ -0,0 +1,97 @@
---
title: Self Hosting
description: Run ClassroomIO on your own infrastructure — with Docker, DigitalOcean, Railway, Coolify, or Dokploy.
---
ClassroomIO is open source, and you can run the whole platform on your own infrastructure. Every
option below runs the same stack — the API, the dashboard, and the background jobs worker, backed by
Postgres and Redis.
Pick the path that matches where you want it to run.
<CardGroup cols={3}>
<Card title="Docker" href="/self-hosted/docker" icon="container">
Run anywhere Docker runs. Pull the pre-built images, or build from source.
</Card>
<Card title="DigitalOcean" href="/self-hosted/digitalocean" icon="droplet">
A plain VPS, end to end — Docker Compose plus Caddy for automatic HTTPS.
</Card>
<Card title="Railway" href="/self-hosted/railway" icon="train-front">
Deploy to Railway from a template. The quickest managed option.
</Card>
<Card title="Coolify" href="/self-hosted/coolify" icon="server">
Self-hosted PaaS on your own VPS, from a Docker Compose resource.
</Card>
<Card title="Dokploy" href="/self-hosted/dokploy" icon="ship">
Self-hosted PaaS on your own VPS, deployed from a raw Docker Compose file.
</Card>
</CardGroup>
## Which should I choose?
| Option | Best for |
| ------------------------------------- | --------------------------------------------------------------------- |
| [Docker](/self-hosted/docker) | Full control on your own server, or as the base for another platform. |
| [DigitalOcean](/self-hosted/digitalocean) | A raw VPS with no PaaS — you set up the reverse proxy and HTTPS yourself. Works on any Ubuntu server. |
| [Railway](/self-hosted/railway) | Getting a working instance up with the least infrastructure work. |
| [Coolify](/self-hosted/coolify) | Running a PaaS on your own VPS, managing several apps alongside it. |
| [Dokploy](/self-hosted/dokploy) | Running a Traefik-based PaaS on your own VPS, deployed from a pasted Compose file. |
:::note[You need an SMTP provider]
Signup verification, password resets, and invite emails all send over SMTP. Whichever option you
pick, have mail credentials ready before you start.
:::
## What you'll need
- A **Postgres** database and a **Redis** instance (both are included in the Docker and Coolify compose files).
- An **SMTP provider** for transactional email.
- Object storage (**MinIO** or S3-compatible) if you want file and video uploads.
- *Optional:* an **OpenAI API key** for transcription and AI course generation.
For the full environment variable reference — secrets, email, storage, CSP, integrations, licensing,
and versions — see the [Configuration](/self-hosted/configuration) section, which documents every
variable the stack reads.
## Server requirements
ClassroomIO is not a single container — it runs **seven services**: Postgres, Redis, MinIO (object
storage), the API, the dashboard, and a background **jobs worker** that handles video processing,
transcription, and AI course generation. The jobs worker spikes RAM whenever it runs, so a server's
idle memory use understates what you actually need.
| Resource | Minimum | Recommended |
| -------- | ---------------- | ------------- |
| CPU | 2 vCPU | 4 vCPU |
| RAM | 4 GB | **8 GB** |
| Disk | 20 GB + uploads | 40 GB+ SSD |
:::warning[Don't under-provision RAM]
A **1 GB server cannot run ClassroomIO at all** — it exhausts memory on the base stack before the
app is even up. **2 GB is only enough for a PaaS control plane** (Coolify, Dokploy) with little left
over for the app. Target **8 GB for reliable operation**, especially once the jobs worker starts
processing video or AI generation.
:::
:::note[Running a PaaS on the same box?]
Coolify and Dokploy need roughly **2 GB for themselves** on top of ClassroomIO's requirement. If
they share the server, size it for both — e.g. 8 GB for ClassroomIO plus the PaaS overhead.
:::
Building the images **from source** is a separate, heavier job that needs ~8 GB RAM at build time —
see the [Docker guide](/self-hosted/docker#build-from-source). Pulling the pre-built images (the
default) avoids that.
## HTTPS is required in production
:::warning[Serve over HTTPS on any real domain]
ClassroomIO's authentication session cookies are marked `Secure`, so browsers refuse to store or
send them over plain HTTP — **login silently fails**. Plain HTTP works only for local evaluation on
`localhost`. Any internet-facing deployment **must** be served over HTTPS.
:::
Terminate TLS at a reverse proxy in front of the stack:
- **Railway** and **Coolify** issue the certificate and terminate HTTPS for you — nothing extra to do.
- **Raw Docker** on your own server: put **nginx**, **Caddy**, or **Traefik** in front, and set
`DASHBOARD_ORIGIN` / `ORIGIN` to the `https://` URL your users visit.
@@ -1,4 +0,0 @@
{
"title": "Self Hosting",
"pages": ["docker", "railway", "coolify"]
}
+42 -40
View File
@@ -3,9 +3,6 @@ title: Railway
description: Deploy ClassroomIO on Railway with the one-click template.
---
import { Callout } from 'fumadocs-ui/components/callout';
import { Step, Steps } from 'fumadocs-ui/components/steps';
The quickest hosted deployment is the [Railway Template](https://railway.com/deploy/classroomio-1?referralCode=8OyIj5). It provisions the API, dashboard, and jobs worker services plus the Postgres and Redis plugins for you.
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/classroomio-1?referralCode=8OyIj5)
@@ -14,9 +11,7 @@ The quickest hosted deployment is the [Railway Template](https://railway.com/dep
Prefer to follow along? This video walks through the whole deployment end to end. The written steps below mirror it, and the [Environment variable reference](#environment-variable-reference) has every value to copy.
<div style={{ display: 'flex', justifyContent: 'center', marginTop: '16px', marginBottom: '16px' }}>
<iframe width="560" height="315" src="https://www.youtube.com/embed/JQZ1ZikLM20?si=AAdI2Muyb2GcetiS" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen></iframe>
</div>
<YouTube id="JQZ1ZikLM20" />
## Prerequisites
@@ -24,14 +19,16 @@ Prefer to follow along? This video walks through the whole deployment end to end
- An **SMTP provider** (or Zoho/ZeptoMail) for signup verification, password reset, and invite emails. See [SMTP Setup](#smtp-setup).
- *Optional:* a **custom domain** for the dashboard and storage, an **OpenAI API key** (transcription + AI course generation), and Google OAuth / Unsplash keys.
<Callout title="Two secrets to generate first">
You'll reuse `BETTER_AUTH_SECRET` and `PRIVATE_SERVER_KEY` across services. Generate them once and keep them handy:
:::note[Two secrets to generate first]
You'll reuse `BETTER_AUTH_SECRET` and `PRIVATE_SERVER_KEY` across services. Generate them once and keep them handy:
```zsh title="Terminal"
openssl rand -hex 32 # BETTER_AUTH_SECRET
openssl rand -hex 32 # PRIVATE_SERVER_KEY (must be identical in cio-api and cio-dashboard)
```
</Callout>
```zsh title="Terminal"
openssl rand -hex 32 # BETTER_AUTH_SECRET
openssl rand -hex 32 # PRIVATE_SERVER_KEY (must be identical in cio-api and cio-dashboard)
```
See [Configuration → Environment & secrets](/self-hosted/configuration/secrets) for what each does.
:::
## Step-by-step walkthrough
@@ -45,7 +42,7 @@ The one-click template provisions **all of the services** for you — Postgres,
Click the **Deploy on Railway** button above. Railway opens the template page where all of the ClassroomIO services are listed and ready to configure.
![Railway template page for ClassroomIO after clicking Deploy](/railway-01-deploy-template.webp)
![Railway template page for ClassroomIO after clicking Deploy](/docs/railway-01-deploy-template.webp)
</Step>
@@ -59,7 +56,7 @@ The template comes with every service preconfigured — most need nothing from y
The next three steps cover the services that **do** need input: `cio-jobs`, `cio-minio`, and `cio-api`.
![The Railway template listing all preconfigured ClassroomIO services](/railway-02-services-overview.webp)
![The Railway template listing all preconfigured ClassroomIO services](/docs/railway-02-services-overview.webp)
</Step>
<Step>
@@ -73,7 +70,7 @@ The next three steps cover the services that **do** need input: `cio-jobs`, `cio
- `SMTP_SENDER`
- `SMTP_PASSWORD`
![Entering the SMTP variables on the cio-jobs service in the template](/railway-03-jobs-smtp.webp)
![Entering the SMTP variables on the cio-jobs service in the template](/docs/railway-03-jobs-smtp.webp)
</Step>
<Step>
@@ -85,7 +82,7 @@ Set the storage credentials on `cio-minio`:
- `MINIO_ROOT_USER`
- `MINIO_ROOT_PASSWORD`
![Configuring MINIO_SERVER_URL and credentials on the cio-minio service](/railway-04-minio-credentials.webp)
![Configuring MINIO_SERVER_URL and credentials on the cio-minio service](/docs/railway-04-minio-credentials.webp)
</Step>
<Step>
@@ -93,8 +90,8 @@ Set the storage credentials on `cio-minio`:
On `cio-api`, set the SMTP host and any additional values you want. See the [Environment variable reference](#environment-variable-reference) below for the full list (object storage, auth secrets, dashboard origin, and more).
![Configuring the cio-api service variables in the template](/railway-05-api-config.webp)
![Additional cio-api configuration options](/railway-06-api-config-additional.webp)
![Configuring the cio-api service variables in the template](/docs/railway-05-api-config.webp)
![Additional cio-api configuration options](/docs/railway-06-api-config-additional.webp)
</Step>
<Step>
@@ -102,17 +99,17 @@ On `cio-api`, set the SMTP host and any additional values you want. See the [Env
Click **Deploy**. Railway begins building and bringing the services online — Redis, Postgres, and the rest come up first.
![Railway deploying the ClassroomIO services after clicking Deploy](/railway-07-deploying.webp)
![Railway deploying the ClassroomIO services after clicking Deploy](/docs/railway-07-deploying.webp)
</Step>
<Step>
### First boot may crash — this is expected
<Callout type="warn" title="Only if you use the Railway-generated subdomain">
On the first deploy, `cio-api` can crash. This is expected **only when you rely on the Railway-generated subdomain**: the API needs the dashboard's public domain (Better Auth's base URL), which doesn't exist yet. If you're using your **own custom subdomain**, this crash doesn't happen — skip ahead to signing up.
</Callout>
:::warning[Only if you use the Railway-generated subdomain]
On the first deploy, `cio-api` can crash. This is expected **only when you rely on the Railway-generated subdomain**: the API needs the dashboard's public domain (Better Auth's base URL), which doesn't exist yet. If you're using your **own custom subdomain**, this crash doesn't happen — skip ahead to signing up.
:::
![cio-api crashing on first boot before the dashboard domain exists](/railway-08-first-boot-crash.webp)
![cio-api crashing on first boot before the dashboard domain exists](/docs/railway-08-first-boot-crash.webp)
</Step>
<Step>
@@ -120,7 +117,7 @@ Click **Deploy**. Railway begins building and bringing the services online — R
Open **cio-dashboard → Settings → Networking** and click **Generate Domain**. Railway assigns the public domain the API needs.
![cio-dashboard Settings and Networking with the Generate Domain button](/railway-09-generate-domain.webp)
![cio-dashboard Settings and Networking with the Generate Domain button](/docs/railway-09-generate-domain.webp)
</Step>
<Step>
@@ -128,8 +125,8 @@ Open **cio-dashboard → Settings → Networking** and click **Generate Domain**
Redeploy **cio-dashboard**, then **cio-api**, so both pick up the newly generated domain. They should now boot cleanly.
![Redeploying the cio-dashboard](/railway-10-redeploy-dashboard.webp)
![Redeploying the cio-api](/railway-11-redeploy-api.webp)
![Redeploying the cio-dashboard](/docs/railway-10-redeploy-dashboard.webp)
![Redeploying the cio-api](/docs/railway-11-redeploy-api.webp)
</Step>
<Step>
@@ -137,7 +134,7 @@ Redeploy **cio-dashboard**, then **cio-api**, so both pick up the newly generate
Open the generated dashboard URL in a **new browser tab** and **sign up** to create your first account.
![The running ClassroomIO dashboard sign-up page on the Railway domain](/railway-12-signup.webp)
![The running ClassroomIO dashboard sign-up page on the Railway domain](/docs/railway-12-signup.webp)
</Step>
</Steps>
@@ -197,7 +194,7 @@ ALLOWED_EXTERNAL_DOMAINS="https://fonts.googleapis.com"
Optional: add `PUBLIC_APP_TITLE`, `PUBLIC_APP_DESCRIPTION`, `PUBLIC_OG_IMAGE_URL` for custom branding. You can also use per-directive CSP vars (`CSP_SCRIPT_SRC_DOMAINS`, `CSP_MEDIA_SRC_DOMAINS`, etc.) instead of `ALLOWED_EXTERNAL_DOMAINS` for finer control.
**Upload size limits** (optional): add the same `UPLOAD_MAX_*_MB` values to **cio-api** and **cio-dashboard** (e.g. `UPLOAD_MAX_DOCUMENT_MB=50`). See [Docker self-hosting → Upload file size limits](/docs/self-hosted/docker#optional--upload-file-size-limits).
**Upload size limits** (optional): add the same `UPLOAD_MAX_*_MB` values to **cio-api** and **cio-dashboard** (e.g. `UPLOAD_MAX_DOCUMENT_MB=50`). See [Configuration → Upload file size limits](/self-hosted/configuration/storage#upload-file-size-limits).
### cio-jobs (background worker)
@@ -222,9 +219,9 @@ SMTP_HOST= SMTP_PORT= SMTP_USER= SMTP_PASSWORD= SMTP_SENDER=
OPENAI_API_KEY=
```
<Callout title="PRIVATE_SERVER_KEY">
`PRIVATE_SERVER_KEY` must be the same value in both API and dashboard. It authenticates server-side dashboard requests to the API. Referencing `${{cio-api.PRIVATE_SERVER_KEY}}` in the dashboard keeps them in sync.
</Callout>
:::note[PRIVATE_SERVER_KEY]
`PRIVATE_SERVER_KEY` must be the same value in both API and dashboard. It authenticates server-side dashboard requests to the API. Referencing `${{cio-api.PRIVATE_SERVER_KEY}}` in the dashboard keeps them in sync.
:::
### cio-minio (object storage)
@@ -245,17 +242,22 @@ The api and jobs services reach MinIO over **two** endpoints: the private `OBJEC
(browser-facing presigned downloads and public images) — already wired in the env blocks above.
`OBJECT_STORAGE_FORCE_PATH_STYLE=true` is required for MinIO.
<Callout type="warn" title="MinIO is your durability boundary">
Single-node MinIO has no redundancy — the Railway Volume is the only thing protecting your
uploads. Back it up. If you'd rather not operate storage, use external **Cloudflare R2 / AWS S3**
instead (R2 has no egress fees): point the same `OBJECT_STORAGE_*` vars at the provider. See the
[Cloudflare Setup](/docs/contributor-guides/cloudflare-setup) guide for R2.
</Callout>
:::warning[MinIO is your durability boundary]
Single-node MinIO has no redundancy — the Railway Volume is the only thing protecting your uploads, so
back it up. See [Configuration → Object storage & uploads](/self-hosted/configuration/storage) for
external Cloudflare R2 / AWS S3 alternatives.
:::
### SMTP Setup
You need SMTP for email sending. See [Nodemailer SMTP docs](https://nodemailer.com/smtp/) for provider setup.
See [Configuration → Email (SMTP)](/self-hosted/configuration/email) for the SMTP variables and provider setup.
### SSO (Enterprise)
SSO requires a license key. Set `LICENSE_KEY` in your API environment. [Book a call](http://cal.com/classroomio/enterprise) to get a license.
SSO requires a `LICENSE_KEY` on the `cio-api` service. See [Configuration → SSO & licensing](/self-hosted/configuration/sso).
## Upgrades
Set `CIO_VERSION` on the ClassroomIO services to pin a release, then redeploy to upgrade. See
[Configuration → Versions & upgrades](/self-hosted/configuration/versions) for pinning, rollback, and
the pre-upgrade backup steps.
+8 -10
View File
@@ -3,8 +3,6 @@ title: Student Dashboard
description: Learn how students navigate the LMS, including Home, My Learning, Cohorts, and Explore.
---
import { Callout } from 'fumadocs-ui/components/callout';
The student dashboard (LMS) is where students open courses, track progress, and discover new content. If you are an admin, this page helps you explain what students see after they join your organization.
Students reach the LMS after they sign in on your academy. The left sidebar lists the main areas below. **Exercise** and **Community** only appear when your organization has those features enabled.
@@ -42,9 +40,9 @@ Both show enrolled courses. My Learning adds clearer filtering for students mana
**Explore** lists **published, active courses** in your organization that the student has **not** enrolled in yet. Each card links to the course landing page so they can read details and enroll (if enrollment is open).
<Callout type="info" title="Explore is a catalog, not an enrollment gate">
A course can appear in Explore even when public self-enrollment is turned off. **Allow New Students** controls whether someone can join from the landing page, not whether the course is visible in Explore. To restrict who can join, see [How Course Enrollment Works](/docs/course-enrollment).
</Callout>
:::info[Explore is a catalog, not an enrollment gate]
A course can appear in Explore even when public self-enrollment is turned off. **Allow New Students** controls whether someone can join from the landing page, not whether the course is visible in Explore. To restrict who can join, see [How Course Enrollment Works](/guides/course-enrollment).
:::
Explore is useful when you want students to discover optional or elective courses without assigning them manually.
@@ -58,7 +56,7 @@ Explore is useful when you want students to discover optional or elective course
| Courses not yet joined | Explore |
| Multi-course tracks with goals | Cohorts |
For how admins create cohorts and set goals, see [Cohorts](/docs/cohorts).
For how admins create cohorts and set goals, see [Cohorts](/cohorts).
## What students see right after enrolling
@@ -82,7 +80,7 @@ If you added them from **People** but they have not signed in yet, they may show
## Related guides
- [How Course Enrollment Works](/docs/course-enrollment) — how students join courses and what settings control access
- [How to Invite Students](/docs/invite-students) — send enrollment links by email
- [Cohorts](/docs/cohorts) — multi-course tracks and goals for admins
- [Create a Course Landing Page](/docs/course-landingpage) — what prospects see before enrolling
- [How Course Enrollment Works](/guides/course-enrollment) — how students join courses and what settings control access
- [How to Invite Students](/guides/invite-students) — send enrollment links by email
- [Cohorts](/cohorts) — multi-course tracks and goals for admins
- [Create a Course Landing Page](/guides/course-landingpage) — what prospects see before enrolling
+4 -6
View File
@@ -3,8 +3,6 @@ title: Tags
description: Organize and filter courses with organization-scoped tags in ClassroomIO.
---
import { Callout } from 'fumadocs-ui/components/callout';
Tags in ClassroomIO help teams structure course catalogs and improve discovery across admin
and student experiences.
@@ -63,7 +61,7 @@ Current tags-related endpoints include:
- `PUT /course/:courseId/tags`
- `GET /organization/courses/public?siteName=<siteName>&tags=<...>`
<Callout title="In Progress">
Archive/restore lifecycle, advanced facet semantics, and expanded LMS filtering UX are being
incrementally rolled out.
</Callout>
:::note[In Progress]
Archive/restore lifecycle, advanced facet semantics, and expanded LMS filtering UX are being
incrementally rolled out.
:::
+5 -10
View File
@@ -3,22 +3,17 @@ title: Token Auth
description: Sign users in to ClassroomIO from your own app with a short-lived JWT.
---
import { Callout } from 'fumadocs-ui/components/callout';
import { Step, Steps } from 'fumadocs-ui/components/steps';
# Token Auth
Token Auth lets an external app sign a user in to ClassroomIO without asking for a ClassroomIO password. Your backend signs a short-lived JWT with your organization's Token Auth secret, then redirects the user's browser to ClassroomIO's token exchange URL.
Use Token Auth when you already authenticate users in another product and want a trusted handoff into your ClassroomIO academy.
<Callout type="info" title="Enterprise Feature">
Token Auth requires an **Enterprise** plan.
:::info[Enterprise Feature]
Token Auth requires an **Enterprise** plan.
**Cloud:** [Contact us](mailto:support@classroomio.com) to upgrade.
**Cloud:** [Contact us](mailto:support@classroomio.com) to upgrade.
**Self-hosted:** Requires an Enterprise license key. Set `LICENSE_KEY` in the API service environment.
</Callout>
**Self-hosted:** Requires an Enterprise license key. Set `LICENSE_KEY` in the API service environment — see [Self Hosting → SSO & licensing](/self-hosted/configuration/sso).
:::
## How It Works
+15 -30
View File
@@ -2,44 +2,29 @@
"name": "@cio/docs",
"type": "module",
"scripts": {
"dev": "react-router dev",
"build": "react-router build",
"preview": "wrangler dev --port 4173",
"start": "wrangler dev --port 3000",
"prebuild": "node scripts/fetch-openapi.mjs",
"dev": "node scripts/fetch-openapi.mjs && blume dev",
"build": "blume build && node scripts/package-assets.mjs",
"preview": "pnpm run build && wrangler dev --port 4173",
"deploy": "pnpm run build && wrangler deploy",
"cf-typegen": "wrangler types && react-router typegen",
"types:check": "fumadocs-mdx && wrangler types && react-router typegen && tsc --noEmit",
"postinstall": "fumadocs-mdx"
"validate": "blume validate"
},
"dependencies": {
"@astrojs/mdx": "7.0.3",
"@cio/utils": "workspace:*",
"@scalar/core": "^0.3.28",
"@react-router/dev": "^7.9.4",
"fumadocs-core": "16.2.1",
"fumadocs-mdx": "14.0.4",
"fumadocs-ui": "16.2.1",
"isbot": "^5.1.30",
"lucide-react": "^0.562.0",
"lucide-static": "^0.552.0",
"react": "^19.2.0",
"react-router": "^7.9.4",
"react-dom": "^19.2.0",
"tailwind-merge": "^3.3.1",
"vite": "^7.2.0"
"@scalar/astro": "0.4.9",
"@shikijs/twoslash": "4.3.1",
"@tailwindcss/typography": "0.5.19",
"@tailwindcss/vite": "4.1.18",
"astro": "7.0.9",
"blume": "1.1.2",
"mermaid": "^11.16.0",
"tailwindcss": "4.1.18"
},
"devDependencies": {
"@cloudflare/vite-plugin": "1.15.3",
"@tailwindcss/vite": "^4.1.16",
"@types/mdx": "^2.0.13",
"@types/node": "^24.10.0",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"tailwindcss": "^4.1.16",
"typescript": "^5.9.3",
"vite-tsconfig-paths": "^5.1.4",
"wrangler": "4.86.0"
},
"engines": {
"node": "^20.19.3"
"node": ">=22.12.0"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

-9
View File
@@ -1,9 +0,0 @@
import type { Config } from '@react-router/dev/config';
export default {
ssr: true,
basename: '/docs',
future: {
v8_viteEnvironmentApi: true
}
} satisfies Config;
+66
View File
@@ -0,0 +1,66 @@
/**
* Fetches the public API OpenAPI spec from R2 and enriches it with the bearer
* auth scheme, so Blume can render the API reference statically at build time.
*
* This replaces the old SSR route that fetched the spec on every request. The
* page now refreshes when the docs are rebuilt — .github/workflows/upload-openapi-spec.yml
* triggers a docs deploy after it publishes a new spec.
*
* If the fetch fails but a previously fetched spec is on disk, that one is
* reused: a network blip should not stop you building or previewing the docs.
* With no spec at all there is nothing to render, so the build fails.
*/
import { access, mkdir, writeFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { enrichPublicApiOpenApiSpec, PUBLIC_API_OPENAPI_URL } from '@cio/utils/openapi/public-api';
const OUTPUT = resolve(dirname(fileURLToPath(import.meta.url)), '../openapi/public-api.json');
const exists = async (path) => {
try {
await access(path);
return true;
} catch {
return false;
}
};
let response;
try {
response = await fetch(PUBLIC_API_OPENAPI_URL);
} catch (error) {
if (await exists(OUTPUT)) {
console.warn(
`[docs] Could not reach ${PUBLIC_API_OPENAPI_URL} (${error.cause?.code ?? error.message}). Reusing the spec already in openapi/.`
);
process.exit(0);
}
throw new Error(
`Failed to reach the OpenAPI spec at ${PUBLIC_API_OPENAPI_URL}, and no previously fetched copy exists in openapi/. The API reference cannot be built.`,
{ cause: error }
);
}
if (!response.ok) {
if (await exists(OUTPUT)) {
console.warn(
`[docs] ${PUBLIC_API_OPENAPI_URL} returned ${response.status} ${response.statusText}. Reusing the spec already in openapi/.`
);
process.exit(0);
}
throw new Error(
`Failed to fetch the OpenAPI spec from ${PUBLIC_API_OPENAPI_URL}: ${response.status} ${response.statusText}`
);
}
const spec = enrichPublicApiOpenApiSpec(await response.json());
await mkdir(dirname(OUTPUT), { recursive: true });
await writeFile(OUTPUT, `${JSON.stringify(spec, null, 2)}\n`);
console.log(`[docs] Wrote enriched OpenAPI spec to ${OUTPUT}`);
+57
View File
@@ -0,0 +1,57 @@
/**
* Packages `blume build` output for the assets-only Cloudflare Worker.
*
* Two things have to be reconciled:
*
* 1. `deployment.base: '/docs'` makes Blume emit URLs like `/docs/foo`, but it
* does NOT nest the output — pages land in `dist/foo/index.html`. The worker
* is asked for `/docs/foo` (the website proxies the full path through), so
* the build is moved under `build/docs/` to line up with the served paths.
*
* 2. Astro's own `redirects` are unusable under a base: it prefixes a
* redirect's source but not its target, emitting `url=/guides` rather than
* `/docs/guides`. So the redirects are written here as a Cloudflare
* `_redirects` file, which must sit at the root of the assets directory.
*/
import { readdirSync } from 'node:fs';
import { cp, mkdir, rm, writeFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const dist = resolve(root, 'dist');
const out = resolve(root, 'build');
const content = resolve(root, 'content/docs');
const slugs = (dir) =>
readdirSync(resolve(content, dir), { withFileTypes: true })
.filter((e) => e.isFile() && e.name.endsWith('.mdx') && e.name !== 'index.mdx')
.map((e) => e.name.replace(/\.mdx$/, ''));
// Guides moved from `/docs/<slug>` to `/docs/guides/<slug>` (Blume scopes a tab's
// sidebar by route prefix). The old URLs are public, so forward them.
const rootSlugs = new Set(slugs('.'));
const GUIDE_REDIRECTS = slugs('guides')
// A slug that is also a root page would shadow it — `_redirects` beats assets.
.filter((slug) => !rootSlugs.has(slug))
.map((slug) => `/docs/${slug} /docs/guides/${slug} 301`)
.join('\n');
const REDIRECTS = `# Generated by scripts/package-assets.mjs — do not edit.
/docs/ /docs 301
/docs/home /docs 301
/docs/how-to-guides/* /docs/:splat 301
/docs/api/reference /docs/api 301
/docs/contributor-guides /docs/guides/contributing 301
/docs/contributor-guides/* /docs/guides/contributing/:splat 301
${GUIDE_REDIRECTS}
`;
await rm(out, { recursive: true, force: true });
await mkdir(out, { recursive: true });
await cp(dist, resolve(out, 'docs'), { recursive: true });
await writeFile(resolve(out, '_redirects'), REDIRECTS);
console.log('[docs] Packaged dist/ -> build/docs/ with _redirects');
-7
View File
@@ -1,7 +0,0 @@
import { defineConfig, defineDocs } from 'fumadocs-mdx/config';
export const docs = defineDocs({
dir: 'content/docs'
});
export default defineConfig();
-34
View File
@@ -1,34 +0,0 @@
{
"include": [
".react-router/types/**/*",
"app/**/*.ts",
"app/**/*.tsx",
"workers/**/*.ts",
"worker-configuration.d.ts",
"react-router.config.ts",
"source.config.ts",
"vite.config.ts"
],
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"types": ["vite/client"],
"isolatedModules": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"target": "ES2022",
"allowJs": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"rootDirs": [".", "./.react-router/types"],
"paths": {
"@/*": ["./app/*"],
"fumadocs-mdx:collections/*": [".source/*"]
},
"noEmit": true
}
}

Some files were not shown because too many files have changed in this diff Show More