# hyprcrd A self-hosted card/note manager with markdown, tags, image galleries, and Obsidian-compatible export. ## Requirements - PHP 8.3+ - Apache with `mod_rewrite` - PHP extensions: `pdo_sqlite`, `zip` No external dependencies. All assets are bundled locally. ## Quick Start (Docker) ```bash git clone https://git.uesome.de/tanztee/hyprcrd.git cd hyprcrd docker build -t hyprcrd . docker run -d -p 8080:80 -v hyprcrd-data:/var/www/html/data hyprcrd ``` Open `http://localhost:8080` and set your password on first visit. The `-v hyprcrd-data:/var/www/html/data` flag persists the database across container restarts. ### Via docker compose If you prefer compose, the bundled `docker-compose.yml` starts the app alone by default: ```bash docker compose up -d # just the app on :8088 ``` To also bring up a local relay (for multi-tenant messaging / collab / chat), opt in via the `relay` profile: ```bash docker compose --profile relay up -d # app on :8088 + relay on :8081 ``` Most installs talk to a remote relay, so the app-only default keeps the local setup minimal. ## Manual Installation ### 1. Clone ```bash git clone https://git.uesome.de/tanztee/hyprcrd.git cd hyprcrd ``` ### 2. Set document root Point your web server's document root to the `public/` directory. **Apache vhost example:** ```apache ServerName hyprcrd.local DocumentRoot /path/to/hyprcrd/public AllowOverride All Require all granted ``` Make sure `mod_rewrite` is enabled: ```bash a2enmod rewrite systemctl restart apache2 ``` ### 3. Set permissions ```bash mkdir -p data public/uploads chmod 755 data chmod 777 public/uploads ``` The web server user (e.g. `www-data`) needs write access to both directories. ### 4. PHP settings (optional) For larger image uploads, adjust in `php.ini`: ```ini upload_max_filesize = 20M post_max_size = 22M ``` ### 5. Open and set password Navigate to your URL. On first visit, you'll be prompted to set a password (min. 4 characters). That's it. ## Directory Structure ``` hyprcrd/ ├── config.php # App constants (auto-configured) ├── Dockerfile ├── docker-entrypoint.sh ├── dist.sh # Build distribution ZIP ├── src/ │ ├── bootstrap.php # Initialization + plugin discovery │ ├── Database.php # SQLite schema & migrations │ ├── PluginManager.php # Plugin scanner & asset/slot renderer │ ├── CalDavClient.php # CalDAV HTTP client + iCal parser │ └── CalDavSync.php # Bidirectional CalDAV sync engine ├── public/ # Document root │ ├── index.php # Entry point & API │ ├── .htaccess # URL rewriting │ ├── css/ │ │ ├── style.css # App styles │ │ ├── tailwind.out.css # Generated Tailwind utilities │ │ ├── fonts.css # Local @font-face rules │ │ └── vendor/ # Phosphor Icons CSS + fonts │ ├── fonts/ # JetBrains Mono & Silkscreen │ ├── js/ │ │ ├── hyprcrd.js # Plugin API (window.hyprcrd, sync-loaded) │ │ ├── app.js # Orchestrator │ │ ├── modules/ # Core feature modules │ │ └── vendor/ # marked.js, DOMPurify │ ├── plugins/ # Optional, drop-in plugin folders │ │ ├── canvas/ # ASCII art canvas editor │ │ ├── chat/ # Encrypted relay chat │ │ ├── collab-buckets/ # Collaborative bucket sync │ │ ├── email/ # Email overlay │ │ ├── gitea/ # Gitea issue integration │ │ ├── logseq-import/ # Logseq markdown importer │ │ ├── slideshow/ # Presentation mode │ │ └── tests/ # UI + state test runner │ └── uploads/ # User media (writable) ├── electron/ # Electron desktop app wrapper │ ├── main.js # Spawns PHP, creates BrowserWindow │ ├── router.php # PHP built-in server router │ ├── fetch-php.sh # Downloads static PHP binaries │ └── php-bin/ # Platform-specific PHP binaries ├── relay/ # Relay service (multi-tenant messaging + collab) │ ├── index.php # Entry point + API dispatch │ ├── inc/ # Backend modules (bootstrap, crypto, auth, routes) │ ├── ui/ # Frontend (layout.php, style.css, app.js) │ ├── .htaccess # URL rewriting + data protection │ ├── Dockerfile │ └── dist.sh # Build relay distribution ZIP └── data/ └── hyprcrd.db # SQLite database (auto-created) ``` ## Features - **Cards** with markdown body, color, tags, and position - **Buckets** for organizing cards into groups - **Tags** written as `#tagname` or hierarchical `#parent/child` in card body, extracted automatically - **Markdown** with `[[Card Title]]` wiki-links, ```` ```gallery ```` image galleries, ```` ```tree ```` list views, ```` ```ascii ```` art canvas (via canvas plugin) - **Search** combining text and `#tag` filters (e.g. `#project meeting notes`) - **Assets gallery** (images, audio, PDFs) with orphan detection and fullscreen viewer - **Split view** showing tag-related and content-related cards - **Import/Export** as JSON+media ZIP or Obsidian/Logseq-compatible markdown vault - **Drag & drop** card reordering and grouping - **Calendar** with month view, journal entries, appointments, and day popup - **CalDAV sync** with ownCloud/Nextcloud (bidirectional, `#termin` tagged cards) - **Inline calculator** (`?3+5` → `8`, `34+56?` → `34+56=90`) - **Snippets** (`?dat`, `?time`, `?now`, `?todo`, `?table`, `?code`, ...) - **Due dates** with overdue notifications and pulsing cards - **Relay** for mobile note capture and card sharing between instances - **Notification banner** for upcoming events, birthdays, overdue tasks - **Board split view** with side-by-side preview - **Superview** compact multi-column list with hover cards - **Super-buckets** — nest buckets one level deep with a dropdown for children - **Card groups** — drag-to-group, join multiple cards, dissolve - **Context menu** — color, tags, copy text, mark done, join cards, make ticket (gitea) - **Find & replace** — in-card (Ctrl+H) and across all cards (Ctrl+Shift+H via search) - **Arrow key navigation** on the board with Enter to open - **PDF upload** in the editor toolbar - **Keyboard shortcuts**: `Ctrl+K` search, `F1` help, `?` shortcut reference, arrow keys, 1–9 bucket switch - **Electron desktop app** — standalone AppImage/DMG/NSIS with bundled PHP, no server needed ## Plugins hyprcrd has a drop-in plugin system. A plugin is just a folder under `public/plugins/` with a `plugin.json` manifest. The core scans the directory at startup, loads each plugin's CSS/JS/HTML, and the plugin self-registers against `window.hyprcrd`. Plugins are entirely optional — removing a plugin folder removes the feature, with no code changes anywhere else. ### Bundled plugins - **`canvas/`** — ASCII art canvas editor. Adds a toolbar button + ` ```ascii ` fenced block. Draw with brush, line, rectangle, ellipse, fill, and text tools using a monospace character grid. - **`chat/`** — Encrypted chat between relay tenants. Footer flyout with delivery + read receipts. - **`collab-buckets/`** — Collaborative buckets shared between relay tenants with exclusive card locking and E2E encrypted sync. - **`email/`** — Email overlay (toggled with `M` key). - **`gitea/`** — Gitea integration: create issues from cards, link to repos. - **`slideshow/`** — Slideshow presentation mode for cards with `slide-image` blocks. - **`logseq-import/`** — Import Logseq markdown journals and pages. - **`tests/`** — UI + state test runner (40+ test files). Loaded only when `Ctrl+Shift+Alt+T` is pressed, so production users pay no cost for it. ### Plugin layout ``` public/plugins/myplugin/ ├── plugin.json # Manifest (required) ├── myplugin.js # Entry point (loaded as ES module) ├── myplugin.css # Optional styles ├── myplugin.html # Optional HTML fragment (overlays, modals, etc.) └── backend.php # Optional PHP hooks ``` **`plugin.json`:** ```json { "name": "myplugin", "version": "1.0.0", "description": "What this plugin does", "frontend": { "js": ["myplugin.js"], "css": ["myplugin.css"], "html": ["myplugin.html"] }, "backend": "backend.php" } ``` ### Plugin API (`window.hyprcrd`) Loaded synchronously before any module, so plugins can register handlers immediately: | Namespace | Methods | Purpose | |---|---|---| | `hyprcrd.editor` | `registerCommand(name, fn)`, `getCommand(name)`, `api.{body, insertMd, updatePreview}` | Add toolbar commands. The `data-md=""` button dispatches to the registered handler. `api` is set after the editor initializes. | | `hyprcrd.markdown` | `registerFencedBlock(lang, fn)`, `registerPreProcessor(fn)`, `registerPostProcessor(fn)` | Add ` ``` ` block renderers (returns HTML, passes through DOMPurify). Pre/post processors run on the source/HTML around `marked.parse()`. | | `hyprcrd.hooks` | `on(name, fn)`, `off(name, fn)`, `emit(name, data)` | Generic event bus. Use for cross-plugin or core→plugin signaling. | | `hyprcrd.util` | `$(idOrSel)`, `$$(sel)`, `esc(s)` | Lightweight DOM/escape helpers so plugins don't need to import core modules. | | `hyprcrd.registerPlugin(meta)` | — | Register the plugin's metadata for introspection. | ### Minimal plugin example `public/plugins/hello/plugin.json`: ```json { "name": "hello", "version": "1.0.0", "description": "Inserts a greeting", "frontend": { "js": ["hello.js"] } } ``` `public/plugins/hello/hello.js`: ```js const h = window.hyprcrd; h.editor.registerCommand('hello', () => { h.editor.api.insertMd('Hello, world! ', '', ''); }); // Inject a toolbar button const btn = document.createElement('button'); btn.dataset.md = 'hello'; btn.className = 'toolbar-btn'; btn.title = 'Hello'; btn.textContent = '👋'; document.getElementById('md-toolbar').appendChild(btn); h.registerPlugin({ name: 'hello', version: '1.0.0' }); ``` That's the entire plugin. Drop the folder in, reload, the button appears. ### Backend hooks (PHP) If a plugin needs server-side code, set `"backend": "backend.php"` in the manifest. The file is `require_once`'d during bootstrap. Backend plugins can use `PluginManager::instance()->addToSlot('slot-name', '')` to inject HTML into named slots in `index.php` (e.g. `md-toolbar`). ### Removing a plugin ```bash rm -rf public/plugins/canvas ``` That's it. The toolbar button, overlay, CSS, and markdown handler all disappear together. The core has no awareness of any specific plugin. ## Data & Backup All data lives in two places: - `data/hyprcrd.db` — SQLite database (cards, buckets, settings) - `public/uploads/` — uploaded images and audio Back up both to preserve everything. The database auto-migrates on startup. ## Export Formats - **JSON export** (`export` button) — full backup with media, re-importable - **Markdown vault** (`vault` button) — Obsidian/Logseq-compatible structure: ``` Bucket Name/ Card Title.md # YAML frontmatter + markdown body attachments/ image.jpg ``` ## CalDAV Sync (ownCloud / Nextcloud) hyprcrd can sync appointment cards with any CalDAV server (tested with ownCloud 10.15.3.0). ### Setup 1. Open the **calendar** (button in header) 2. Click the **gear icon** in the calendar header 3. Enter your CalDAV URL, username, and app password: ``` URL: https://cloud.example.com/remote.php/dav/calendars/your-user/personal/ Username: your-user Password: your-app-password ``` 4. Click **test** to verify the connection 5. Click **save** ### How it works - Cards tagged with `#termin` are synced as calendar events - The appointment card template maps to iCalendar fields: ```markdown Title: 2026-04-15 — Team Meeting **Zeit:** 14:30-15:30 **Ort:** Room 42 **Teilnehmer:** Alice, Bob --- **Notizen:** Agenda items here #termin ``` - **Sync is bidirectional**: local changes push to the server, remote changes pull to hyprcrd - **Conflict resolution**: last-modified timestamp wins - **Deleted cards**: tracked via tombstones, deleted on the server on next sync - **Auto-sync**: triggers when opening the calendar (max once per minute) - **Manual sync**: click the cloud icon in the calendar header ### Security - Credentials are encrypted at rest (AES-256-CBC) in the SQLite database - All sync operations run server-side (PHP/cURL), no client-side credential exposure - Supports HTTPS with certificate verification ## Calendar The built-in calendar shows cards on their dates: - **Journal cards** (title = `2026-04-08`) appear on that date - **Appointment cards** (title = `2026-04-08 — Subject`) appear on their date - **Other cards** fall back to their creation date - **Click a day** to see all cards in a popup - **Double-click** or use **+ journal** / **+ termin** to create new entries - Appointment template includes Zeit, Ort, Teilnehmer, Notizen fields - Navigate months with arrow buttons, **today** jumps to current date ## Vault Key (Multi-Instance Encryption) When running multiple hyprcrd instances that share CalDAV credentials, you need a shared encryption key. ### Setup 1. Go to **Settings > Account > Vault Key** 2. Enter the same key (min 8 characters) on all instances 3. Click **save** ### How it works - The vault key encrypts/decrypts CalDAV credentials stored in the database - Key derivation: `SHA-256(vault_key)` → deterministic, same on all instances - Without a vault key, encryption falls back to the login password hash (single-instance only) - Changing the vault key automatically re-encrypts CalDAV credentials - Changing the login password does NOT affect encryption when a vault key is set ### Important - The vault key is stored in plaintext in each instance's database — it is NOT synced - You must enter it manually on each connected instance ## Relay A self-hosted **multi-tenant** message relay for hyprcrd. Multiple users share one relay instance — each one has their own login, their own client encryption secret, and their own inbox. Cards (and in the future: encrypted chat messages) can be sent **between tenants** on the same relay. **Full documentation:** [`relay/README.md`](relay/README.md) ### Architecture ``` [hyprcrd alice] ──push──┐ ┌──poll── [hyprcrd alice] ├──► [Relay] ◄────────────┤ [hyprcrd bob] ──push──┘ tenants: alice, bob └──poll── [hyprcrd bob] sender encrypts → relay decrypts with sender's secret → re-encrypts with shared inter-tenant key → stores → on recipient poll: decrypts → re-encrypts with recipient's secret ``` ### Deploy **Webspace** (any PHP host with `mod_rewrite` and `pdo_sqlite`): ```bash cd relay ./dist.sh # upload hyprcrd-relay.zip and extract into your webspace ``` **Docker**: ```bash cd relay docker build -t hyprcrd-relay . docker run -d -p 8081:80 -v relay-data:/var/www/html/data hyprcrd-relay ``` The `data/` directory is created on first request and contains the SQLite DB plus the master encryption key (chmod 600, blocked from web access via `.htaccess`). ### Development (both together) ```bash ./dev.sh ``` Starts hyprcrd on `http://localhost:8088` and the relay on `http://localhost:8081` in the same Docker network. Source files are bind-mounted — changes are live without rebuilding. From inside the hyprcrd container, the relay is reachable at `http://relay:80`. Under the hood this runs `docker compose --profile relay up --build`. The relay service lives behind the `relay` compose profile, so a plain `docker compose up` brings up only the app — handy when you're developing against a remote relay. ### Setup workflow 1. Open the relay URL **from a desktop browser** — the setup wizard is desktop-friendly 2. Create the first **admin** account: username + password (UI login) + shared secret (client encryption) 3. As admin, click the **users** tab and create accounts for any additional users you want to give relay access to 4. Each user enters their credentials in their own hyprcrd: **Settings → Relay** → relay URL + username + shared secret 5. To send a card from one tenant to another, use the relay UI's compose form (recipient picker) — or push cards from hyprcrd ### How it works (high level) - **Per-tenant inboxes**: each user only sees their own messages - **Cross-tenant routing**: send a card to another tenant on the same relay; the relay decrypts with the sender's secret, re-encrypts with a shared inter-tenant key for storage, then re-encrypts with the recipient's secret on delivery - **At-rest encryption**: every sensitive value in the DB (tenant secrets, the inter-tenant key, message bodies) is encrypted. The master key lives outside the DB in a chmod-600 file - **24-hour ephemeral**: messages and attachments auto-delete after a day. Cards extracted in hyprcrd are the long-term store - **Encrypted chat**: tenant-to-tenant chat with delivery + read receipts (via chat plugin). Per-pair E2E key exchange is on the roadmap ### Relay Security - Per-tenant bcrypt-hashed passwords + secrets - Master key encryption for tenant secrets and the inter-tenant key at rest - HttpOnly + SameSite=Strict session cookies - Rate-limited login (5 attempts/minute per IP) - AES-256-CBC for all message encryption - Security headers (X-Frame-Options, HSTS, X-Content-Type-Options) - `data/` directory blocked from direct download via `.htaccess` - Request size limited to 256KB (15MB for attachments) - Last-admin protection prevents accidental lockout For the full security model (including what's NOT protected and the roadmap to strict E2E for chat), see [`relay/README.md`](relay/README.md). ### Migrating from the old single-tenant relay When you upgrade an existing relay installation, the new code automatically detects the legacy schema and drops the old `messages` and `attachments` tables (the data is 24h-ephemeral anyway). The setup wizard appears on your next visit. You'll need to: 1. Run through the desktop setup wizard to create the first admin 2. Recreate any other tenant accounts that need access 3. On every connected hyprcrd, update **Settings → Relay** to add the new **username** field and the new shared secret The old `relay_key` was bcrypt-hashed and cannot be recovered, so all client secrets need to be reissued. ## Electron Desktop App hyprcrd can run as a standalone desktop app via Electron — no web server needed. ```bash cd electron ./fetch-php.sh # download static PHP binary (~20MB) npm install npm start # dev mode npm run dist # build AppImage / DMG / NSIS ``` The app bundles a static PHP binary per platform and stores user data (database + uploads) outside the app bundle: - **Linux**: `~/.config/hyprcrd/data/` - **macOS**: `~/Library/Application Support/hyprcrd/data/` - **Windows**: `%APPDATA%/hyprcrd/data/` See [`electron/README.md`](electron/README.md) for details. ## Rebuilding Tailwind CSS After changing Tailwind utility classes in HTML or JS files: ```bash # Download standalone CLI (once) curl -sL https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64 -o tailwindcss && chmod +x tailwindcss # Build (includes plugins so plugin HTML/JS contributes utility classes) ./tailwindcss --input public/css/tailwind.css --output public/css/tailwind.out.css --content "public/index.php,public/js/**/*.js,public/plugins/**/*.{js,html}" --minify ``` ## Security - Bcrypt password hashing - CSRF protection on all mutations - Rate-limited login (5 attempts/60s) - Upload directory blocks PHP execution - DOMPurify sanitizes all rendered markdown - Security headers (X-Frame-Options, HSTS, etc.) - AES-256-CBC encryption for CalDAV credentials and Relay messages - Vault key system for multi-instance CalDAV encryption (independent of login password) - Relay: separate password + shared secret, rate-limited, client-side encryption ## License MIT