Last Updated: 2026-05-06 Status: Active
Single-page reference for every term used across the blog docs. If a doc uses a term you don't recognize, look here first.
1. Identity & Auth
Tenant
A workspace. Every blog row is owned by exactly one tenant via the tenant_id column. Two tenants can have the same slug, the same category names, and never see each other's data. The id is opaque (e.g. tenant_abc123) and is set by manager-service at signup.
tenant_id
The string that scopes every query in blog-service. Injected into the request context by the gateway via the x-tenant-id header — never read directly from the body or URL. If it's missing, requireTenant middleware rejects with 400.
user_id
The acting user. Used as the author_id on new posts. Comes from the JWT sub field via the gateway as x-user-id. Optional — public API requests have no user.
JWT
JSON Web Token. The seller dashboard's session contains an access_token issued by manager-service, signed with an RSA private key. Gateway verifies it with the matching public key (JWT_PUBLIC_KEY env). Payload includes tenant_id, user_id, permissions[]. See permissions.md.
API Key
A pk_* (publishable) string customers embed in their site to call public blog endpoints. Issued per tenant; gateway maps it back to a tenant on every request. The sk_* (secret) variant exists but is rejected by the SDK at construction time — never use it client-side.
Gateway Secret
The shared string in both apps/gateway/.dev.vars and apps/blog-service/.dev.vars (GATEWAY_SECRET). Sent as x-gateway-key on every internal call. Blog-service rejects any request without a valid match — that's the only thing that prevents direct internet access to the worker. Comma-separated values are allowed for rotation.
system:owner
The wildcard permission. Whoever has it bypasses all requirePermission() checks in blog-service. Workspace owners get this from manager-service automatically.
2. Content
Post
A row in blog_posts. Has a title, slug, content_json, status, tenant_id, optional category_id, and many tags via blog_post_tags. See database.md §2.
content_json
JSONB column holding the TipTap document tree. Shape: { type: "doc", content: [...] }. The admin API returns this raw; the public API converts it to HTML on every request via tiptap-renderer.ts. HTML is never persisted — only JSON.
Slug
URL-safe identifier derived from title. Lowercased, non-alphanumeric replaced with -, leading/trailing - stripped. Unique within a tenant. On collision, blog-service appends -1, -2, etc. (up to 100 attempts), then falls back to a UUID fragment.
WARNING
Slug is regenerated on title change — old URLs break. There is no redirect handling. See database.md §2.
Status
One of "draft", "published", "scheduled". The public API filters to published_at IS NOT NULL, so scheduled posts are invisible until their scheduled_for time passes (auto-publish cron is not yet implemented — see blog-service.md §12).
Excerpt
Optional short summary stored alongside the post. Falls back to seo_description for meta tags. Plain text only — no markup.
Featured Image
The cover/header image, stored as a full URL in featured_image_url. The dashboard upload step rehosts external images to media-service R2 — see markdown-import.md §5.
3. Taxonomy
Category
One per post (or none). Tenant-scoped. Deleting a category sets category_id = NULL on every affected post (no cascade delete of posts).
Tag
Many per post via blog_post_tags junction. Tenant-scoped. Deleting a tag cascade-deletes its junction rows. Tag updates on a post are delete-all-then-insert inside a transaction.
4. Editor
TipTap
The ProseMirror-based React editor (v2). Wraps a stack of extensions — see editor.md §2 for the full inventory.
Node
A block in the document tree (paragraph, heading, image, callout, etc.). Each has a type, optional attrs, and child content.
Mark
An inline formatting span (bold, italic, link, code, highlight). Marks attach to text, not blocks.
Slash Command
The / palette in the editor. Powered by @tiptap/suggestion + Tippy.js. See editor.md §4 for how to add one.
Custom Node View
A React component that replaces TipTap's default rendering for a specific node type. The editor uses these for Image, CodeBlock, Callout, Toggle, Carousel — see editor.md §3.
5. SDK
@vlozi/blog
The npm package customers install. Four entry points: headless (@vlozi/blog), React (@vlozi/blog/react), server components (@vlozi/blog/server), Next.js helpers (@vlozi/blog/next). See sdk-reference.md §1.
VloziClient
The headless API class. new VloziClient({ apiKey, baseUrl }). Server-safe. Methods: client.blog.list(), .get(), .archive(), .related(), .neighbors(), .categories.list(), .tags.list().
VloziProvider
React context wrapping the client + provider config (mermaidTheme, syntaxHighlighting, imageComponent). Required for SDK hooks; not needed for server components.
Hydration
When the SDK rendererfinds a <pre><code class="language-mermaid"> or <div data-type="carousel"> in sanitized HTML, it portal-mounts a live React component into that element. See sdk-reference.md §6 for the data-vlz-* attribute hooks.
Sanitization
Two-pass defense. Server-side (primary): apps/blog-service/src/utils/tiptap-renderer.ts whitelists tags/attrs/protocols. Client-side (defense-in-depth): the SDK's 5-pass sanitizeHtml strips scripts, inline handlers, and non-allowlisted iframes. See sdk-security-model.md.
6. Infrastructure
Cloudflare Worker
The runtime for both gateway and blog-service. JavaScript on V8 isolates, no Node APIs. Local dev runs via wrangler dev.
Service Binding
Cloudflare's in-process call mechanism. Gateway invokes blog-service through env.BLOG_SERVICE.fetch(req), not over HTTP. Result: zero network latency, no public exposure of blog-service. Configured in wrangler.toml — names must match exactly.
Neon
Serverless Postgres provider. Two databases per project: BLOG_DATABASE_URL (blog-service owns) and CORE_DATABASE_URL (manager-service owns; blog-service reads tenant info).
Drizzle
Type-safe SQL ORM. Schema lives in apps/blog-service/src/db/schema.ts. Migrations in drizzle/ are append-only — never edit, always generate.
PGlite
In-process Postgres for tests. Runs inside Node.js with no external service. Used by every blog-service test. See testing.md §3.
Hono
The HTTP framework blog-service is built on. v4. Lightweight, Workers-native, Zod-friendly.
makeTestApp
Test helper that builds the full Hono app wired to a PGlite instance + a fake request context (tenant, user, permissions). Lets route handler tests go through the full middleware pipeline. See testing.md §3.
7. Acronyms
| Acronym | Meaning |
|---|---|
| BC | Backwards-compatible / backwards-compatibility |
| CSP | Content Security Policy — HTTP header controlling what the browser may load |
| IDOR | Insecure Direct Object Reference — attack class where users access others' data by guessing IDs |
| ISR | Incremental Static Regeneration (Next.js) |
| JSONB | Postgres binary-encoded JSON column type |
| JWT | JSON Web Token |
| RSC | React Server Components |
| SDK | Software Development Kit — here, @vlozi/blog |
| SSR | Server-Side Rendering |
| SWR | Stale-While-Revalidate — cache pattern, serve stale until refresh completes |
| XSS | Cross-Site Scripting — injecting executable scripts into rendered content |
8. Where to read next
- End-to-end flows: user-journey.md
- API surface: blog-service.md
- Data shape: database.md
- Permissions: permissions.md