A comprehensive course and event booking application with a full CRUD API. This platform allows users to browse and book available courses and events, while providing an admin section for efficient management of all aspects, including users, courses, sessions, and lessons.
The application is composed of a server-side API built with Nuxt and a client-side interface, also built with Nuxt, offering a seamless and integrated user experience.
- RESTful API: Built with Nuxt, providing robust CRUD operations for all entities.
- Database Integration: Utilizes a SQLite database with schema managed by Drizzle ORM.
- Authentication & Authorization: Secure user authentication via the
nuxt-layers/authlayer (supports Azure AD and no-auth modes) and role-based access control for admin functionalities. - Automated Migrations: Database migrations are automatically run on server startup using the
server/plugins/migrateDb.tsplugin. - Email Services: Transactional emails (registration confirmations, cancellations, custom notifications) rendered via Handlebars templates in
server/templates/. - ICS Calendar Support: Generates and versions iCalendar files for session bookings.
- OpenAPI Documentation: Auto-generated API docs available at
/_openapi.json,/_scalar, and/_swagger(ReDoc).
The client-side application is the user-facing front-end of the Coursebook platform, built as a Single Page Application (SPA) using Nuxt 4, Vue 3, and TypeScript. It is responsible for presenting data, handling user interactions, managing authentication state, and providing an intuitive interface.
- Nuxt 4: The meta-framework for Vue.js, used for its powerful features like file-based routing, server-side rendering (SSR) capabilities, and a rich module ecosystem.
- Vue 3: The progressive JavaScript framework for building user interfaces.
- TypeScript: A typed superset of JavaScript that compiles to plain JavaScript, enhancing code quality and maintainability.
- TailwindCSS &
@nuxt/uiv4: For styling and UI components, providing a consistent and customizable design system. motion-v: For creating smooth animations and transitions within the application.@formkit/nuxt: For building powerful, accessible, and schema-driven forms.@nuxtjs/i18n: For internationalization, allowing the application to support multiple languages.@nuxtjs/mdc: For rendering Markdown content components.@dcc-bs/*libraries: Shared packages for common UI components (@dcc-bs/common-ui.bs.js) and dependency injection (@dcc-bs/dependency-injection.bs.js).- DCC-BS Nuxt Layers: Remote layers providing authentication (
DCC-BS/nuxt-layers/auth) and logging (DCC-BS/nuxt-layers/logger).
The client-side application communicates with the backend API (also part of the Nuxt project) to fetch and manipulate data, ensuring a seamless full-stack experience.
The app/ directory contains all the client-side code, organized to promote modularity and maintainability.
components/: Reusable Vue components.admin/: Components for the admin dashboard:AdminCourseCard.vue,AdminCourseForm.vue— Course management.AdminSessionCard.vue,AdminSessionForm.vue— Session management.AdminLessonCard.vue,AdminLessonForm.vue— Lesson management.AdminUserCard.vue,AdminUserForm.vue— User management.AdminRegistrationsCard.vue— Registration overview.AdminHeader.vue— Admin navigation header.FormEditor.vue— Custom registration form editor.NotifyConfirmModal.vue— Confirmation modal for session notifications.
- Shared Components:
DateTime.vue,ErrorView.vue,LoadingView.vue,NavigationMenu.vue,SessionView.vue(handles registration/unregistration).
composables/: Vue composables for state management and logic reuse (courses.composable.ts,sessions.composable.ts,me.composable.ts,users.composable.ts,useApiFetch.composable.ts).layouts/: Different page structures.default.vue: Standard layout for public pages.admin.vue: Admin layout with role-based access control (checksisAdminviafetchMe()).auth.vue: Pass-through layout wrapper (slot only) for authentication pages provided by thenuxt-layers/authlayer.
pages/: File-based routing.- Public/User-Facing Pages:
index.vue: Homepage with course listing, search, and filtering.courses/[id].vue: Course detail page with session information.courses/[courseId]/[sessionId].vue: Session detail page.me.vue: User profile/dashboard showing registrations and upcoming sessions.
- Auth Pages (provided by
nuxt-layers/authlayer, not inapp/pages/):- Sign-in page is handled by the remote auth layer.
- Admin Pages (
/admin/):index.vue: Admin dashboard with statistics and quick actions.users.vue: User management.courses/: Course management (create, edit, list).courses/[id]/sessions/: Session management for a course.courses/[id]/sessions/[sessionId]/notify.vue: Send notifications to registered users.
- Public/User-Facing Pages:
types/: TypeScript type definitions.utils/: Client-side utility functions (e.g.,course.utils.ts,dateFormat.utils.ts.ts).
- User Interface (UI):
- Design System: Built using
@nuxt/uiv4 components styled with TailwindCSS. - Responsive Design: Fully responsive for an optimal experience on all devices.
- Animations: Uses
motion-vfor smooth UI animations.
- Design System: Built using
- Internationalization (i18n):
- Setup: Handled by
@nuxtjs/i18n. - Locales: Translation files in
i18n/locales/(en.json,de.json). Default is German. - Usage:
useI18n()composable for translations.
- Setup: Handled by
- Authentication & Authorization:
- Authorization: Client-side route protection in
admin.vuelayout. - User State:
useMecomposable for managing authenticated user's state.
- Authorization: Client-side route protection in
- Data Fetching & State Management:
- Composables: Data fetching logic encapsulated in composables (
useCourses,useCourse,useSession,useSetSession,useMe,useUsers). - State: Managed via reactive references (
ref,computed) and Nuxt'suseState. - API Interaction:
useSchemaFetch/fetchWithSchemacomposables (fromuseApiFetch.composable.ts) for schema-validated API calls, or directuseFetch/$fetchfor unvalidated calls.
- Composables: Data fetching logic encapsulated in composables (
- Forms:
- Form Building: Using
@formkit/nuxtand@nuxt/ui. - Validation: Zod schemas for form data validation.
- Input Masking:
maskafor formatted input fields. - Dynamic Forms:
FormEditor.vueallows admins to create custom JSON schema-based registration forms.
- Form Building: Using
- Navigation:
- Component:
NavigationMenu.vuefor primary links. - Routing: Nuxt's file-based routing. Programmatic navigation with
useRouter()andnavigateTo().
- Component:
- Discovery: User lands on
index.vue, views and filters/searches courses. - Course Details: User clicks a course to view
courses/[id].vue, seeing full details and sessions. - Session Selection: User reviews sessions on the course page.
- Registration: User clicks "Register" in
SessionView.vue.- If a custom form exists, it's rendered in a drawer for completion.
- Otherwise, registration is immediate.
- Confirmation: API call registers the user. Success message is shown, and UI updates.
- Calendar: User can download an ICS file for the booked session.
- Login: Admin logs in with Azure AD credentials.
- Admin Dashboard: Admin is redirected to
/admin. - Course Management: Admin navigates to
/admin/coursesto list courses. - Create/Edit Course:
- Create: Admin goes to
/admin/courses/create, fills outAdminCourseForm.vue, and submits to create via API. - Edit: Admin clicks "Edit" on a course, pre-fills
AdminCourseForm.vueon/admin/courses/[id]/edit, and submits to update via API.
- Create: Admin goes to
- Session & Lesson Management: Admin manages sessions and lessons for a course through form-based interfaces under
/admin/courses/[id]/sessions/. - Notifications: Admin can send custom notifications to registered users from
/admin/courses/[id]/sessions/[sessionId]/notify.
- Bun (recommended) or Node.js
- A running SQLite database (or use the default local file-based DB)
bun installCopy the .env.schema file to .env and fill in the required values. The project uses Varlock with the ProtonPass plugin for secret management.
| Variable | Required | Description |
|---|---|---|
APP_MODE |
Yes | Application mode: dev, build, ci, or prod |
AUTH_MODE |
Yes | Authentication mode: none (no auth) or azure (Azure AD) |
DATABASE_URL |
Yes | SQLite database path (e.g., data/coursebooker.db) |
NUXT_AZURE_AUTH_SECRET |
Yes* | Secret key for Azure AD session encryption (required when AUTH_MODE=azure) |
NUXT_AZURE_AUTH_CLIENT_ID |
Yes* | Azure AD client ID (required when AUTH_MODE=azure) |
NUXT_AZURE_AUTH_TENANT_ID |
Yes* | Azure AD tenant ID (required when AUTH_MODE=azure) |
NUXT_AZURE_AUTH_CLIENT_SECRET |
Yes* | Azure AD client secret (required when AUTH_MODE=azure) |
NUXT_AZURE_AUTH_ORIGIN |
No* | Auth origin URL for Azure AD (required when AUTH_MODE=azure) |
SMTP_HOST |
No | SMTP server host (default: localhost) |
SMTP_PORT |
No | SMTP server port (default: 1030) |
MAIL_FROM |
No | Sender email address for outgoing mails (default: noreply@example.com) |
DEFAULT_ADMIN |
No | Default admin user email |
NUXT_SITE_URL |
No | Site URL (default: http://localhost:3000) |
LOG_LEVEL |
No | Log level: trace, debug, info, warn, error, fatal |
* Required only when AUTH_MODE=azure.
bun run devOn initial setup and after running database migrations, a default admin user is automatically created to facilitate access to the admin panel. Set the DEFAULT_ADMIN environment variable to a valid email that exists in the Entra organization.
- Make changes to the database schema in
shared/schema/. - Run
bun run db:generateto create a new migration file. - The migration file will be created in the
drizzle/folder.
The server/plugins/migrateDb.ts plugin will automatically run the migrations on server start.
Production:
docker compose up -dThe app is exposed on port 8502 (mapped to internal port 3000). Data is persisted in the ./data volume.
Development (mail testing):
bun run docker:dev- To test email sending locally: Run
bun run docker:devand open http://localhost:1080 to access the mail testing interface.
All mail templates live in server/templates/ and use Handlebars syntax. Each template exports an html and a text variant (for rich and plain-text email clients respectively).
| File | Purpose |
|---|---|
server/templates/registration.ts |
Sent when a user registers for a session |
server/templates/unregister.ts |
Sent when a user unregisters from a session |
server/templates/cancellation.ts |
Sent when a session is cancelled by an admin |
server/templates/custom-notification.ts |
Sent when an admin sends a custom message to registered users |
server/templates/partials/course-details.ts |
Shared partials: course details block and signature (used by all templates) |
- Open the template file in
server/templates/(e.g.,registration.ts). - Edit the exported
xxxHtmlstring for the HTML variant and thexxxTextstring for the plain-text variant. Both must always be kept in sync. - You can use any Handlebars syntax (
{{variable}},{{#if condition}}...{{/if}},{{> partialName}}). - Restart the dev server to pick up changes:
bun run dev.
All templates receive a context built by buildCourseContext() (defined in server/utils/mail.utils.ts). The following variables are available in every template:
| Variable | Type | Description |
|---|---|---|
givenName |
string | Recipient's first name |
familyName |
string | Recipient's last name |
courseTitle |
string | Title of the course/event |
courseTypeLabel |
string | Localized label: "Kurs" or "Event" |
dateStr |
string | Formatted date(s) of lessons (e.g., 01.06.2026 or numbered list) |
timeStr |
string or null | Formatted time range for single-lesson sessions (e.g., 09:00 - 17:00) |
isSingleLesson |
boolean | Whether the session has only one lesson |
location |
string | Session location (defaults to "Nicht bekannt") |
teamsLink |
string or null | MS Teams link if set |
siteUrl |
string | Base URL of the application |
courseId |
string | Course ID (for building links) |
sessionId |
string | Session ID (for building links) |
organizerName |
string | Name of the course organizer |
organizerMail |
string | Email of the course organizer |
Some templates receive additional variables:
| Variable | Template(s) | Type | Description |
|---|---|---|---|
hasIcsAttachment |
registration, custom-notification | boolean | Whether an ICS calendar file is attached |
customMessage |
custom-notification | string | Admin-written custom message |
- Create a new file in
server/templates/(e.g.,server/templates/my-template.ts). - Export two strings:
myTemplateHtmlandmyTemplateTextusing Handlebars syntax. Reuse the shared partials:import { courseDetailsHtml, courseDetailsText, signatureHtml, signatureText } from "./partials/course-details"; export { courseDetailsHtml, courseDetailsText, signatureHtml, signatureText }; export const myTemplateHtml = `...{{> courseDetailsHtml}}...{{> signatureHtml}}...`; export const myTemplateText = `...{{> courseDetailsText}}...{{> signatureText}}...`;
- Register the template in
server/utils/template.utils.ts:- Add the new template name to the
TemplateNameunion type. - Import and add the HTML/text sources to the
templateSourcesrecord.
- Add the new template name to the
- Call
renderTemplate("my-template", context)fromserver/utils/mail.utils.tsin a new or existing send function. - If the template uses new context variables, extend the
extrasparameter inbuildCourseContext().
- Start the mail dev container:
bun run docker:dev - Start the dev server:
bun run dev - Trigger the email you want to test (e.g., register for a session via the UI or API).
- Open http://localhost:1080 to view the sent emails (HTML and plain text) in the MailDev interface.
- To open Drizzle Studio for database inspection and querying, run
bun run db:studioand navigate to https://local.drizzle.studio.
| Script | Description |
|---|---|
bun run dev |
Start development server |
bun run build |
Build for production |
bun run start |
Start production server |
bun run lint |
Format code with Biome |
bun run check |
Lint and format with Biome |
bun run db:generate |
Generate a new migration |
bun run db:push |
Push schema changes directly to DB |
bun run db:studio |
Open Drizzle Studio |
bun run db:migrate |
Run migrations manually |
bun run env:check |
Validate environment variables |
bun run docker:dev |
Start mail dev container |
bun run docker:dev:down |
Stop mail dev container |
See ./server/README.md for detailed API documentation. Interactive API docs are also available at /_scalar and /_swagger when the server is running.