TutorLoom
The learning platform for companies, colleges and training institutes. Free and open source, with an Enterprise edition.
翻译准备中
尚未收集仓库信息。
尚未检测到更新。GitHub 发布版本后会显示在这里。
制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗Comcal is a decentralized community management and calendar platform built on the Nostr protocol. It enables communities to create events, coordinate activities, and share calendars without relying on centralized services. All data is sovereign and stored on Nostr relays—your community maintains full control.
Comcal (Community Calendar) provides communities with a sovereign, decentralized alternative to centralized calendar and community management tools. Built on the Nostr protocol, it implements:
Communities maintain full autonomy—no central server, no data lock-in, no platform dependency. Users authenticate with their private keys, ensuring complete control over their identity and data.
| Category | Technology | Version |
|---|---|---|
| Runtime | Node.js | v22.16.0 |
| Frontend Framework | SvelteKit | 2.1.2 |
| Svelte Version | Svelte | 5.0.0 |
| Styling | TailwindCSS | 3.4.10 |
| Components | DaisyUI | 4.12.10 |
| State Management | Svelte 5 Runes | Native |
| Protocol | Nostr | NIP-52, Communikey NIP |
| Nostr Libraries | Applesauce Suite | Latest |
| Deployment | Docker + Traefik | Production-ready |
| Adapter | @sveltejs/adapter-node | 6.0.0 |
If you use NixOS or have Nix installed, the project includes a flake.nix that provides the exact development environment:
# Clone the repository
git clone <repository-url> comcal
cd comcal
# Allow direnv (auto-activates the environment)
direnv allow
# Install dependencies
pnpm install
The flake provides Node.js 22 and pnpm. With direnv configured, the environment activates automatically when you enter the project directory.
First-time Nix setup: Ensure you have direnv and nix-direnv installed, and flakes enabled in your Nix config (experimental-features = nix-command flakes).
# Clone the repository
git clone <repository-url> comcal
cd comcal
# Use correct Node version (if using nvm)
nvm use
# Install dependencies
pnpm install
# Start the dev server (runs on http://localhost:5173)
pnpm run dev
# Or open automatically in browser
pnpm run dev -- --open
# Build the application
pnpm run build
# Preview production build locally
pnpm run preview
# Development
pnpm run dev # Start dev server with hot reload
# Production
pnpm run build # Build for production (Node adapter)
pnpm run preview # Preview production build
# Code Quality
pnpm run lint # Run ESLint
pnpm run format # Format code with Prettier
# Docker
docker compose build # Build Docker image
docker compose up -d # Start containers in background
docker compose logs -f # View logs
pnpm run devpnpm run lint to check code qualitypnpm run format to maintain style consistency$state, $derived, $effectComcal supports customizable themes for institutional branding:
| Theme | Description |
|---|---|
light | Default light theme |
dark | Default dark theme |
stil | STIL institutional branding (orange accent) |
stil-dark | STIL dark variant |
Users can choose between theme families and light/dark/system color modes via Settings.
Configuration (.env):
THEME_DEFAULT_LIGHT=stil # Default theme for light mode
THEME_DEFAULT_DARK=stil-dark # Default theme for dark mode
Icons are organized in src/lib/components/icons/ by category (ui, actions, social, calendar) with a consistent wrapper pattern using currentColor for theme compatibility.
See CLAUDE.md for detailed developer documentation on theming and icons.
Comcal is designed for production deployment using Docker and Traefik. This section covers deploying to a VPS or self-hosted server.
See Quick Start section above.
# Update system packages
sudo apt update && sudo apt upgrade -y
# Install Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
# Install Docker Compose v2+
sudo apt install docker-compose-plugin
# Add user to docker group (optional, for non-root docker access)
sudo usermod -aG docker $USER
newgrp docker
# Clone the repository
git clone <repository-url> comcal
cd comcal
Comcal uses a 12-factor app approach with runtime environment variables for configuration. This allows you to use the same Docker image across different environments (development, staging, production) by simply changing environment variables.
# Copy environment template
cp .env.example .env
# Edit environment variables
nano .env
# Your domain (required for SvelteKit CSRF protection)
ORIGIN=https://your-domain.com
# Internal service port (usually 3000)
PORT=3000
# Node environment
NODE_ENV=production
The .env.example file contains all available configuration options. Key categories:
App Branding
APP_NAME: Application name (default: "ComCal")APP_LOGO: URL to application logoAPP_GIT_REPO: Git repository URLNostr Relays
FALLBACK_RELAYS: Fallback relays for event discovery (used when gated mode is off)CALENDAR_RELAYS: App-specific relays for calendar events (kinds 31922-31925)COMMUNIKEY_RELAYS: App-specific relays for community events (kinds 10222, 30222)AMB_RELAYS: Educational content relays with NIP-50 search support (kind 30142)LONGFORM_CONTENT_RELAY: App-specific relay for articles (kind 30023)Gated Mode
GATED_MODE_DEFAULT: Default state for gated mode (true/false). When enabled, app fetches only from app-specific relaysGATED_MODE_FORCE: When true, users cannot disable gated mode - useful for institutional deploymentsCurated Mode
CURATED_PUBKEYS_SETS: Comma-separated NIP-19 naddr identifiers pointing to kind 30000 follow sets. When set, only content from pubkeys in these follow sets is shown for primary content (calendar events, AMB resources, articles, community definitions). Social content (reactions, comments, chat, targeted publications) is not filtered. Leave empty to disable.CURATED_PUBKEYS: Comma-separated hex pubkeys or npub-encoded pubkeys for direct curated author filtering. These are unioned with pubkeys from CURATED_PUBKEYS_SETS. If either has entries, curated mode is active. Simpler than follow sets for small deployments.WoT (Web of Trust) Mode
WoT extends curated mode by using anchor pubkeys' follow graphs. Anchor pubkeys' kind 3 contact lists are fetched at startup, and their follows become allowed authors. The effective author set is the union of curated authors, WoT anchors, WoT follows, and (optionally) the logged-in user's own follows.
WOT_ENABLED: Enable WoT content filtering (true/false, default: false)WOT_INCLUDE_USER_FOLLOWS: Include logged-in user's own follows as allowed authors (true/false, default: true)WOT_ANCHOR_PUBKEYS: Global anchor pubkeys (hex or npub, comma-separated). These pubkeys + their follows become allowed authors for all categoriesWOT_ANCHOR_PUBKEYS_CALENDAR: Per-category anchor override (replaces global for calendar). Same pattern for _COMMUNIKEY, _EDUCATIONAL, _LONGFORM, _KANBANCalendar Settings
CALENDAR_WEEK_START_DAY: Week start day (0=Sunday, 1=Monday)CALENDAR_LOCALE: deprecated, ignored since the scheduled-meetings release — calendar names follow the app languageCALENDAR_TIME_FORMAT: Time format (12h or 24h)Interactive Apps (webxdc)
Sandboxed interactive learning apps (webxdc .xdc packages) run in the resource viewer and — with the NIP-29 channels feature — as shared collaborative sessions inside community channels (e.g. a collaborative pad).
SANDBOX_DOMAIN: Cross-origin sandbox host for running apps (default: iframe.diy). Self-hosting is the documented fallbackWEBXDC_APPS: Curated apps for the channel app picker — comma-separated kind-1063 event references (nevent1… or 64-char hex event ids). Ordered; the first entry is featured prominently (e.g. the collaborative pad). Name, icon, file hash and license all resolve from each referenced event. Publish an app's kind-1063 with node scripts/publish-webxdc-app.mjs --url <blossom-url> --name <name> [--icon <url>] [--license <url>] [--credit <text>] (signs with EDUFEED_PUBLISHER_NSEC, prints the nevent to paste here). Leave empty to offer only live-discovered apps. The published collaborative pad (webxdc/editor v4.0.0, GPL-3.0, hosted on blossom.edufeed.org) is available as a ready-made first entry — see the WEBXDC_APPS example in .env.example for its neventDiscover Page
DISCOVER_CONTENT_TYPES: Which tabs /discover offers, comma-separated from events, learning, articles, boards, communities, people (default: all six). The "All" tab is derived, not configured: it shows up whenever at least two of the feed types (events, learning, articles, boards) are enabled and merges exactly those. Disabled types are not fetched, their tabs and the landing/dashboard links into them are hidden, and ?type=<disabled> falls back to the first tabNIP-29 Channels (Groups)
GROUPS_ENABLED: Feature flag for community channels backed by a NIP-29 groups relay (true/false, default: false)GROUPS_RELAYS: The NIP-29 groups relay (e.g. wss://groups.edufeed.org). Channel chat, membership, and shared webxdc session state (kinds 9450/24450) live hereSignup
SIGNUP_SUGGESTED_COMMUNITIES: Communities pre-checked in the signup step-3 picker (comma-separated npubs or hex pubkeys). Leave empty to disable.Membership & NIP-05 Handles
Lets users apply for a memorable name@<domain> handle during signup. Requires a separately deployed nip-05-service instance — the app proxies admin approvals to it, it does not host its own NIP-05 backend.
MEMBERSHIP_ENABLED: Feature flag (true/false). When false, signup step 4 and the settings card are hiddenNIP05_HANDLE_DOMAIN: Single domain handles are issued under (e.g. edufeed.org). Used for UI labels and for the live availability check against https://<domain>/.well-known/nostr.jsonMEMBERSHIP_FORM_ADDRESS: Kind 30168 form template address in the form 30168:<admin-pubkey-hex>:membership. Published once via scripts/publish-membership-form.jsMEMBERSHIP_ADMIN_PUBKEYS: Hex pubkeys allowed to see /admin/membership (comma-separated). The same allowlist is enforced server-side by /api/nip05 — only NIP-98 events signed by one of these pubkeys are forwardedNIP05_SERVICE_URL: SECRET — Base URL of the standalone nip-05-service. Server-only (never sent to the browser)NIP05_SERVICE_API_KEY: SECRET — Bearer token for the nip-05-service admin API. Server-onlyLogin Methods
| Variable | Default | Purpose |
|---|---|---|
NPUB_LOGIN_ENABLED | false | Read-only "browse as npub" login method |
GOOGLE_LOGIN_ENABLED | false | "Continue with Google" via the Pomegranate FROST threshold signer (promenade) |
POMEGRANATE_CENTRAL_URL | https://auth.njump.me | Central server (Google OAuth + NIP-46 relay + FROST coordinator) |
POMEGRANATE_OPERATOR_URLS | 5 public community operators | Comma-separated shard-operator servers; ≥2 required, signing threshold is ceil(7n/12) |
The Google flow generates a random key client-side, splits it into FROST shards across the operators, and signs via NIP-46 — no server ever holds the whole key. Self-hosting promenade later only requires changing these two URLs.
Media Uploads (Blossom)
BLOSSOM_UPLOAD_ENDPOINT: Blossom server upload endpointBLOSSOM_MAX_FILE_SIZE: Maximum file size in bytes (app-side limit; not enforced by the Blossom server itself)Metadata Cleaner (optional)
Optional quiet opt-in backed by a metadata-cleaner service instance. For supported files (PDF, JPG/JPEG, PNG, TIF/TIFF, WebP), the license modal shows an unchecked "remove hidden file metadata" checkbox (plus a compress select for PDFs) and a "show details" link into a read-only inspect view — no interstitial interrupts the normal upload flow. If ticked, stripping (and compression) happens silently during the deferred upload step and the cleaned bytes go to Blossom; a subtle confirmation note appears on the file row afterwards, and the cleaner failing never blocks the upload. The one interruption is a PDF over BLOSSOM_MAX_FILE_SIZE: it auto-opens a compression-first rescue modal (balanced preselected) before the upload proceeds, since compression may bring it under the limit.
METADATA_CLEANER_URL: SECRET — Base URL of the metadata-cleaner service (e.g. https://cleaner.edufeed.org). Server-only, proxied via /api/metaclean; when unset the feature is hidden entirelyMETADATA_CLEANER_MAX_UPLOAD_MB: Maximum body size the /api/metaclean proxy accepts, in MB (default 200, matching the service's own limit). Deliberately independent of BLOSSOM_MAX_FILE_SIZEBODY_SIZE_LIMIT (default 512K) must be raised above the proxy cap, or uploads to /api/metaclean are rejected before the route runsGeocoding (OpenCage API)
GEOCODING_API_KEY: SECRET - OpenCage API key (never expose to client)GEOCODING_CACHE_DURATION_DAYS: Cache duration for geocoded resultsGEOCODING_MIN_ADDRESS_LENGTH: Minimum address length for geocodingGEOCODING_MIN_CONFIDENCE_SCORE: Minimum confidence score (0-10)GEOCODING_REQUIRE_ADDRESS_COMPONENTS: Require address components (true/false)GEOCODING_ACCEPTED_COMPONENT_TYPES: Accepted component types (comma-separated)Imprint/Legal Information
IMPRINT_ENABLED: Enable/disable imprint pageIMPRINT_ORGANIZATION: Organization nameIMPRINT_ADDRESS_*: Address fieldsIMPRINT_CONTACT_*: Contact informationIMPRINT_*: Other legal informationEducational Content
EDUCATIONAL_SEARCH_DEBOUNCE_MS: Search debounce delayEDUCATIONAL_VOCAB_*: SKOS vocabulary keysSee .env.example for complete documentation of all variables with descriptions and defaults.
.env files - They are in .gitignore by defaultGEOCODING_API_KEY is kept server-side only.env files for dev/staging/productionEdit docker-compose.yml and update:
comcalyour-domain.com with your actual domain in Traefik labelstraefik_web matches your Traefik setupExample Traefik configuration:
labels:
- 'traefik.http.routers.comcal.rule=Host(`your-domain.com`)'
- 'traefik.http.routers.comcal.entrypoints=websecure'
- 'traefik.http.routers.comcal.tls.certresolver=letsencrypt'
# Build the Docker image
docker compose build
# Start the application in detached mode
docker compose up -d
# View real-time logs
docker compose logs -f
Comcal uses the following Nostr event kinds:
| Kind | NIP | Description |
|---|---|---|
| 0 | NIP-01 | User profile (metadata) |
| 3 | NIP-02 | Contact list |
| 5 | NIP-09 | Deletion event |
| 7 | NIP-25 | Reaction |
| 8 | NIP-58 | Badge award |
| 9 | — | Chat message |
| 14 | NIP-17 | Private direct message (rumor) |
| 1059 | NIP-17 | Gift wrap (sealed DM envelope) |
| 1069 | — | Form response |
| 1111 | NIP-22 | Comment |
| 10002 | NIP-65 | Relay list (outbox model) |
| 10063 | — | Blossom server list |
| 10222 | Communikey | Community definition |
| 30002 | NIP-51 | Relay set (user relay overrides) |
| 30009 | NIP-58 | Badge definition |
| 30142 | AMB | Educational resource (OER) |
| 30168 | — | Form template |
| 30222 | Communikey | Targeted publication |
| 30301 | — | Kanban board |
| 30000 | NIP-51 | Follow set (community membership) |
| 31922 | NIP-52 | Date-based calendar event |
| 31923 | NIP-52 | Time-based calendar event |
| 31924 | NIP-52 | Calendar collection |
| 31925 | NIP-52 | Calendar RSVP |
See CLAUDE.md for the full reference including implementation details.
Guides for specific lanes live in docs/guides/ — e.g.
Publishing to a NIP-29 Group.
Edufeed-style deployments hand out memorable name@<domain> handles (NIP-05) to vetted members. The app does not run its own NIP-05 backend — it integrates with a separately deployed nip-05-service.
Components:
/admin/membership): lists pending applications, gated by MEMBERSHIP_ADMIN_PUBKEYS. Each row shows the applicant's profile and decrypted wished handle, with live status against the upstream NIP-05 directory (available / already taken / already mapped to applicant)./api/nip05): SvelteKit endpoint that re-authenticates the admin via NIP-98, verifies their pubkey is on the allowlist, then forwards a POST to the standalone nip-05-service with NIP05_SERVICE_API_KEY. The browser never sees the API key./.well-known/nostr.json lists their handle. One click writes nip05: name@<domain> to their kind 0 profile.Operator setup checklist:
Deploy the standalone nip-05-service (see its own README) and note its base URL and admin Bearer token.
In this app's .env, set MEMBERSHIP_ENABLED=true, NIP05_HANDLE_DOMAIN, MEMBERSHIP_ADMIN_PUBKEYS, NIP05_SERVICE_URL, NIP05_SERVICE_API_KEY.
Bootstrap the form template once with the admin key:
ADMIN_NSEC=nsec1… node scripts/publish-membership-form.js
Copy the printed 30168:<pubkey>:membership address into MEMBERSHIP_FORM_ADDRESS and restart.
Sign in as an admin pubkey and verify /admin/membership is reachable.
docker compose logs for application logsThis project was funded by the BMBSFJ.
Förderkennzeichen: 01PZ24007
Further development happens under funding of Stiftung Innovation in der Hochschullehre:
Contributions are welcome!
Built with ❤️ on the Nostr protocol.