add astro fuma docs for website

Signed-off-by: Arvindh <arvindh91@gmail.com>
This commit is contained in:
Arvindh
2026-02-11 17:41:06 +05:30
parent dce448035d
commit 3a5410bbb4
47 changed files with 13889 additions and 2 deletions
+21
View File
@@ -0,0 +1,21 @@
# dependencies
/node_modules
# build outputs
/dist
/.astro
/coverage
*.tsbuildinfo
# misc
.DS_Store
*.pem
/.pnp
.pnp.js
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# env
.env*.local
.wrangler
+47
View File
@@ -0,0 +1,47 @@
# FluxMQ Web (Astro + Starlight)
This `web` folder is now an Astro static site with:
- Product landing page at `/`
- Documentation powered by Starlight at `/docs/`
- Built-in docs search (`Ctrl+K`) via Starlight + Pagefind
- Tailwind CSS v4 for styling
## Development
```bash
npm install
npm run dev
```
or
```bash
pnpm install
pnpm dev
```
Open `http://localhost:4321`.
## Build
```bash
npm run build
npm run preview
```
or
```bash
pnpm build
pnpm preview
```
## Structure
- `src/pages/index.astro`: product landing page
- `src/components/home/*.astro`: landing page components (Astro-only, no React)
- `src/content/docs/docs/*`: docs content shown under `/docs/*`
- `src/styles/global.css`: Tailwind entry and shared brand/component styles
- `src/styles/starlight.css`: docs-theme overrides for Starlight
- `astro.config.mjs`: Astro, Tailwind, Starlight, and sitemap config
+38
View File
@@ -0,0 +1,38 @@
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import react from '@astrojs/react';
import sitemap from '@astrojs/sitemap';
import tailwindcss from '@tailwindcss/vite';
import {
rehypeCode,
remarkCodeTab,
remarkHeading,
remarkNpm,
remarkStructure,
} from 'fumadocs-core/mdx-plugins';
const site = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://fluxmq.absmach.eu';
export default defineConfig({
site,
base: process.env.FQ_BASE_PATH ?? '/',
output: 'static',
integrations: [
sitemap(),
react(),
mdx({
extendMarkdownConfig: false,
syntaxHighlight: false,
remarkPlugins: [
remarkHeading,
remarkCodeTab,
remarkNpm,
[remarkStructure, { exportAs: 'structuredData' }],
],
rehypePlugins: [rehypeCode],
}),
],
vite: {
plugins: [tailwindcss()],
},
});
+10010
View File
File diff suppressed because it is too large Load Diff
+32
View File
@@ -0,0 +1,32 @@
{
"name": "web",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"check": "astro check"
},
"dependencies": {
"@astrojs/mdx": "^4.3.13",
"@astrojs/react": "^4.4.2",
"@orama/orama": "^3.1.18",
"@astrojs/sitemap": "^3.0.0",
"@tailwindcss/vite": "^4.1.18",
"astro": "^5.5.0",
"fumadocs-core": "^16.5.0",
"fumadocs-ui": "^16.5.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwindcss": "^4.1.18"
},
"devDependencies": {
"@astrojs/check": "^0.9.0",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"typescript": "^5.9.3"
},
"packageManager": "pnpm@10.12.1+sha512.f0dda8580f0ee9481c5c79a1d927b9164f2c478e90992ad268bbb2465a736984391d6333d2c327913578b2804af33474ca554ba29c04a8b13060a717675ae3ac"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

@@ -0,0 +1,48 @@
import { useEffect } from 'react';
import { FrameworkProvider } from 'fumadocs-core/framework';
import { useSearchContext } from 'fumadocs-ui/contexts/search';
import { RootProvider } from 'fumadocs-ui/provider/base';
import SearchDialog from '@/components/search';
declare global {
interface Window {
__fluxmqOpenSearch?: () => void;
}
}
function HomeSearchBridgeInner() {
const { setOpenSearch } = useSearchContext();
useEffect(() => {
const handler = () => setOpenSearch(true);
window.__fluxmqOpenSearch = handler;
window.addEventListener('fluxmq:open-search', handler);
return () => {
delete window.__fluxmqOpenSearch;
window.removeEventListener('fluxmq:open-search', handler);
};
}, [setOpenSearch]);
return null;
}
export default function HomeSearchBridge() {
return (
<FrameworkProvider
usePathname={() => window.location.pathname}
useParams={() => ({})}
useRouter={() => ({
push(nextPathname) {
window.location.assign(nextPathname);
},
refresh() {
window.location.reload();
},
})}
>
<RootProvider search={{ SearchDialog }}>
<HomeSearchBridgeInner />
</RootProvider>
</FrameworkProvider>
);
}
+68
View File
@@ -0,0 +1,68 @@
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { DocsPage, type DocsPageProps } from 'fumadocs-ui/layouts/docs/page';
import type { Root } from 'fumadocs-core/page-tree';
import type { ReactNode } from 'react';
import { FrameworkProvider } from 'fumadocs-core/framework';
import { navigate } from 'astro:transitions/client';
import { RootProvider } from 'fumadocs-ui/provider/base';
import SearchDialog from './search';
export function Docs({
tree,
children,
pathname,
params,
page,
}: {
tree: Root;
children: ReactNode;
pathname: string;
params: Record<string, string | string[]>;
page?: DocsPageProps;
}) {
return (
<FrameworkProvider
usePathname={() => pathname}
useParams={() => params}
useRouter={() => ({
push(nextPathname) {
void navigate(nextPathname);
},
refresh() {
window.location.reload();
},
})}
>
<RootProvider search={{ SearchDialog }}>
<DocsLayout
tree={tree}
nav={{
title: (
<span style={{ fontWeight: 800, lineHeight: 1.1, fontSize: '1.9rem' }}>
<span style={{ color: 'var(--flux-blue)' }}>Flux</span>
<span style={{ color: 'var(--flux-orange)' }}>MQ</span>
</span>
),
url: '/',
}}
links={[
{
type: 'icon',
label: 'github',
text: 'Github',
url: 'https://github.com/absmach/fluxmq',
external: true,
icon: (
<svg role="img" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
</svg>
),
},
]}
>
<DocsPage {...page}>{children}</DocsPage>
</DocsLayout>
</RootProvider>
</FrameworkProvider>
);
}
@@ -0,0 +1,49 @@
<section id="architecture" class="border-b-2 border-theme bg-theme-alt py-20">
<div class="mx-auto w-[min(100%-2.5rem,1200px)]">
<h2 class="mb-12 text-4xl font-bold md:text-5xl">
<span class="border-l-4 border-[var(--flux-orange)] pl-4">ARCHITECTURE</span>
</h2>
<div class="mx-auto max-w-4xl">
<div class="brutalist-border bg-theme overflow-x-auto p-6">
<svg viewBox="0 0 880 370" class="min-w-[760px]" role="img" aria-label="FluxMQ architecture diagram">
<g font-family="JetBrains Mono, ui-monospace, monospace" font-size="13" font-weight="700">
<rect x="335" y="20" width="210" height="48" rx="8" fill="#2f69b3" stroke="#1f1f1f" stroke-width="2"></rect>
<text x="440" y="49" text-anchor="middle" fill="#fff">Setup and configuration</text>
<rect x="70" y="110" width="220" height="52" rx="8" fill="#fafafa" stroke="#1f1f1f" stroke-width="2"></rect>
<rect x="330" y="110" width="220" height="52" rx="8" fill="#fafafa" stroke="#1f1f1f" stroke-width="2"></rect>
<rect x="590" y="110" width="220" height="52" rx="8" fill="#fafafa" stroke="#1f1f1f" stroke-width="2"></rect>
<text x="180" y="142" text-anchor="middle" fill="#101010">TCP/WS/HTTP/CoAP Servers</text>
<text x="440" y="142" text-anchor="middle" fill="#101010">AMQP 1.0 Server</text>
<text x="700" y="142" text-anchor="middle" fill="#101010">AMQP 0.9.1 Server</text>
<rect x="70" y="200" width="220" height="52" rx="8" fill="#fafafa" stroke="#1f1f1f" stroke-width="2"></rect>
<rect x="330" y="200" width="220" height="52" rx="8" fill="#fafafa" stroke="#1f1f1f" stroke-width="2"></rect>
<rect x="590" y="200" width="220" height="52" rx="8" fill="#fafafa" stroke="#1f1f1f" stroke-width="2"></rect>
<text x="180" y="232" text-anchor="middle" fill="#101010">MQTT Broker</text>
<text x="440" y="232" text-anchor="middle" fill="#101010">AMQP Broker 1.0</text>
<text x="700" y="232" text-anchor="middle" fill="#101010">AMQP Broker 0.9.1</text>
<rect x="315" y="290" width="250" height="52" rx="8" fill="#f9a32a" stroke="#1f1f1f" stroke-width="2"></rect>
<text x="440" y="322" text-anchor="middle" fill="#101010">Queue Manager (Bindings + Delivery)</text>
<path d="M440 70 L440 104" stroke="#2f69b3" stroke-width="2"></path>
<path d="M180 162 L180 194M440 162 L440 194M700 162 L700 194" stroke="#2f69b3" stroke-width="2"></path>
<path d="M180 252 L180 280 L315 280M440 252 L440 290M700 252 L700 280 L565 280" stroke="#2f69b3" stroke-width="2"></path>
</g>
</svg>
</div>
</div>
<div class="brutalist-border bg-theme mx-auto mt-12 max-w-3xl p-6">
<h3 class="mono mb-4 text-lg font-bold">KEY COMPONENTS</h3>
<ul class="space-y-3 text-base text-theme-muted">
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-blue)]">▸</span><span><strong>Transport Layer:</strong> Multi-protocol servers (MQTT, AMQP 1.0, AMQP 0.9.1, CoAP, HTTP, WebSocket)</span></li>
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-blue)]">▸</span><span><strong>Protocol Brokers:</strong> FSM-based protocol handlers with zero-copy parsing</span></li>
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-blue)]">▸</span><span><strong>Queue Manager:</strong> Durable queue bindings with FIFO, priority, and topic-based delivery</span></li>
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-blue)]">▸</span><span><strong>Storage:</strong> Persistence for messages and topic indexing</span></li>
</ul>
</div>
</div>
</section>
@@ -0,0 +1,19 @@
---
interface Props {
code: string;
}
const { code } = Astro.props;
---
<div class="code-panel">
<button
class="absolute top-2 right-2 border-2 border-[#3c3c3c] bg-[#191b1d] px-2 py-1 text-xs font-semibold text-white transition hover:border-[var(--flux-orange)] hover:bg-[var(--flux-orange)] hover:text-black"
type="button"
data-copy-btn
aria-label="Copy command"
>
Copy
</button>
<pre><code>{code}</code></pre>
</div>
@@ -0,0 +1,90 @@
<section id="features" class="border-b-2 border-[var(--flux-border)] py-20">
<div class="mx-auto w-[min(100%-2.5rem,1200px)]">
<h2 class="mb-12 text-4xl font-bold md:text-5xl">
<span class="border-l-4 border-[var(--flux-orange)] pl-4">FEATURES</span>
</h2>
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<article class="brutalist-card accent-line p-6 pl-8">
<div class="mb-4 inline-flex size-8 items-center justify-center rounded-sm border-2 border-[var(--flux-blue)] text-[var(--flux-blue)]">
<svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 8h16M4 16h16M8 4v16M16 4v16"></path>
</svg>
</div>
<h3 class="mono mb-3 text-xl font-bold">Multi-Protocol Support</h3>
<p class="text-base leading-relaxed text-theme-muted">
Full MQTT 3.1.1 and 5.0 over TCP and WebSocket, plus HTTP-MQTT and CoAP bridges. All protocols share the
same broker core and messages flow seamlessly across transports.
</p>
</article>
<article class="brutalist-card accent-line p-6 pl-8">
<div class="mb-4 inline-flex size-8 items-center justify-center rounded-sm border-2 border-[var(--flux-orange)] text-[var(--flux-orange)]">
<svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<ellipse cx="12" cy="6" rx="7" ry="3"></ellipse>
<path d="M5 6v6c0 1.7 3.1 3 7 3s7-1.3 7-3V6"></path>
<path d="M5 12v6c0 1.7 3.1 3 7 3s7-1.3 7-3v-6"></path>
</svg>
</div>
<h3 class="mono mb-3 text-xl font-bold">Durable Queues</h3>
<p class="text-base leading-relaxed text-theme-muted">
Persistent message queues with consumer groups, ack/nack/reject semantics, dead-letter queues, and retention
policies. Raft-based replication with automatic failover.
</p>
</article>
<article class="brutalist-card accent-line p-6 pl-8">
<div class="mb-4 inline-flex size-8 items-center justify-center rounded-sm border-2 border-[var(--flux-blue)] text-[var(--flux-blue)]">
<svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="6"></rect>
<rect x="3" y="14" width="18" height="6"></rect>
</svg>
</div>
<h3 class="mono mb-3 text-xl font-bold">Clustering & High Availability</h3>
<p class="text-base leading-relaxed text-theme-muted">
Embedded etcd for coordination, gRPC-based inter-broker communication with mTLS, automatic session
ownership, and graceful shutdown with session transfer.
</p>
</article>
<article class="brutalist-card accent-line p-6 pl-8">
<div class="mb-4 inline-flex size-8 items-center justify-center rounded-sm border-2 border-[var(--flux-orange)] text-[var(--flux-orange)]">
<svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M13 2L4 14h7l-1 8 10-13h-7l0-7z"></path>
</svg>
</div>
<h3 class="mono mb-3 text-xl font-bold">Performance Optimized</h3>
<p class="text-base leading-relaxed text-theme-muted">
Zero-copy packet parsing, object pooling, efficient trie-based topic matching, and direct instrumentation.
300K-500K msg/s per node with sub-10ms latency.
</p>
</article>
<article class="brutalist-card accent-line p-6 pl-8">
<div class="mb-4 inline-flex size-8 items-center justify-center rounded-sm border-2 border-[var(--flux-blue)] text-[var(--flux-blue)]">
<svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 3l7 3v6c0 5-3.5 7.5-7 9-3.5-1.5-7-4-7-9V6l7-3z"></path>
</svg>
</div>
<h3 class="mono mb-3 text-xl font-bold">Security</h3>
<p class="text-base leading-relaxed text-theme-muted">
TLS/mTLS for client connections, mTLS for inter-broker gRPC, DTLS options for CoAP, WebSocket origin
validation, and per-IP/per-client rate limiting.
</p>
</article>
<article class="brutalist-card accent-line p-6 pl-8">
<div class="mb-4 inline-flex size-8 items-center justify-center rounded-sm border-2 border-[var(--flux-orange)] text-[var(--flux-orange)]">
<svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M16 18l6-6-6-6M8 6l-6 6 6 6"></path>
</svg>
</div>
<h3 class="mono mb-3 text-xl font-bold">Open-Source & Extensible</h3>
<p class="text-base leading-relaxed text-theme-muted">
Licensed under Apache 2.0 with a clean layered architecture. Pluggable storage backends, protocol-agnostic
domain logic, and easy extensibility.
</p>
</article>
</div>
</div>
</section>
@@ -0,0 +1,42 @@
<footer class="bg-[var(--flux-bg-alt)] py-14">
<div class="mx-auto grid w-[min(100%-2.5rem,1200px)] gap-6 md:grid-cols-2 xl:grid-cols-4">
<section>
<h3 class="mb-3 text-lg font-bold uppercase">About</h3>
<p class="text-[var(--flux-muted)]">
FluxMQ is developed by Abstract Machines, an IoT infrastructure and security company located in Paris.
</p>
</section>
<section>
<h3 class="mb-3 text-lg font-bold uppercase">Products</h3>
<ul class="space-y-2 text-[var(--flux-muted)]">
<li><a class="hover:text-[var(--flux-orange)]" href="https://magistrala.absmach.eu" target="_blank" rel="noopener noreferrer">Magistrala</a></li>
<li><a class="hover:text-[var(--flux-orange)]" href="https://absmach.eu/supermq/" target="_blank" rel="noopener noreferrer">SuperMQ</a></li>
<li><a class="hover:text-[var(--flux-orange)]" href="https://absmach.eu/propeller/" target="_blank" rel="noopener noreferrer">Propeller</a></li>
</ul>
</section>
<section>
<h3 class="mb-3 text-lg font-bold uppercase">Resources</h3>
<ul class="space-y-2 text-[var(--flux-muted)]">
<li><a class="hover:text-[var(--flux-orange)]" href="/docs/">Documentation</a></li>
<li><a class="hover:text-[var(--flux-orange)]" href="https://github.com/absmach/fluxmq" target="_blank" rel="noopener noreferrer">GitHub</a></li>
<li><a class="hover:text-[var(--flux-orange)]" href="https://absmach.eu/blog/" target="_blank" rel="noopener noreferrer">Blog</a></li>
</ul>
</section>
<section>
<h3 class="mb-3 text-lg font-bold uppercase">Contact</h3>
<ul class="space-y-2 text-[var(--flux-muted)]">
<li><a class="hover:text-[var(--flux-orange)]" href="mailto:info@absmach.eu">info@absmach.eu</a></li>
<li><a class="hover:text-[var(--flux-orange)]" href="https://github.com/absmach" target="_blank" rel="noopener noreferrer">GitHub</a></li>
<li><a class="hover:text-[var(--flux-orange)]" href="https://twitter.com/absmach" target="_blank" rel="noopener noreferrer">Twitter</a></li>
<li><a class="hover:text-[var(--flux-orange)]" href="https://www.linkedin.com/company/abstract-machines" target="_blank" rel="noopener noreferrer">LinkedIn</a></li>
</ul>
</section>
</div>
<div class="mx-auto mt-8 w-[min(100%-2.5rem,1200px)] border-t-2 border-[var(--flux-border)] pt-4 text-center text-sm text-[var(--flux-muted)]">
&copy; 2026 Abstract Machines. Licensed under Apache 2.0.
</div>
</footer>
@@ -0,0 +1,94 @@
<div class="h-full w-full" aria-hidden="true">
<svg viewBox="0 0 760 560" class="h-full w-full" role="img">
<defs>
<marker id="arrow-blue" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
<path d="M0,0 L0,6 L6,3 z" fill="#2f69b3" />
</marker>
<marker id="arrow-green" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
<path d="M0,0 L0,6 L6,3 z" fill="#2a9c4a" />
</marker>
<marker id="arrow-orange" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
<path d="M0,0 L0,6 L6,3 z" fill="#f9a32a" />
</marker>
</defs>
<rect
x="260"
y="60"
width="220"
height="440"
rx="10"
fill="none"
stroke="#f9a32a"
stroke-width="2"
stroke-dasharray="7 8"
opacity="0.8"
/>
<g class="network-dash" style="color:#2f69b3">
<line x1="130" y1="110" x2="280" y2="110" marker-end="url(#arrow-blue)" />
<line x1="130" y1="230" x2="280" y2="260" marker-end="url(#arrow-blue)" />
<line x1="130" y1="350" x2="280" y2="260" marker-end="url(#arrow-blue)" />
<line x1="130" y1="470" x2="280" y2="440" marker-end="url(#arrow-blue)" />
<line x1="130" y1="110" x2="280" y2="440" marker-end="url(#arrow-blue)" />
<line x1="130" y1="230" x2="280" y2="440" marker-end="url(#arrow-blue)" />
</g>
<g class="network-dash" style="color:#f9a32a">
<line x1="370" y1="160" x2="370" y2="240" marker-end="url(#arrow-orange)" />
<line x1="370" y1="340" x2="370" y2="420" marker-end="url(#arrow-orange)" />
<line x1="418" y1="240" x2="418" y2="160" marker-end="url(#arrow-orange)" />
<line x1="418" y1="420" x2="418" y2="340" marker-end="url(#arrow-orange)" />
</g>
<g class="network-dash" style="color:#2a9c4a">
<line x1="460" y1="110" x2="620" y2="140" marker-end="url(#arrow-green)" />
<line x1="460" y1="260" x2="620" y2="280" marker-end="url(#arrow-green)" />
<line x1="460" y1="430" x2="620" y2="420" marker-end="url(#arrow-green)" />
<line x1="460" y1="110" x2="620" y2="420" marker-end="url(#arrow-green)" />
<line x1="460" y1="260" x2="620" y2="140" marker-end="url(#arrow-green)" />
</g>
<g font-family="JetBrains Mono, ui-monospace, monospace" font-size="14" font-weight="700">
<rect x="20" y="84" width="112" height="44" rx="8" fill="rgba(47, 105, 179, 0.07)" stroke="#2f69b3" stroke-width="1.3" />
<rect x="20" y="204" width="112" height="44" rx="8" fill="rgba(47, 105, 179, 0.07)" stroke="#2f69b3" stroke-width="1.3" />
<rect x="20" y="324" width="112" height="44" rx="8" fill="rgba(47, 105, 179, 0.07)" stroke="#2f69b3" stroke-width="1.3" />
<rect x="20" y="444" width="112" height="44" rx="8" fill="rgba(47, 105, 179, 0.07)" stroke="#2f69b3" stroke-width="1.3" />
<text x="76" y="112" text-anchor="middle" fill="#2f69b3">MQTT</text>
<text x="76" y="232" text-anchor="middle" fill="#2f69b3">HTTP</text>
<text x="76" y="352" text-anchor="middle" fill="#2f69b3">WebSocket</text>
<text x="76" y="472" text-anchor="middle" fill="#2f69b3">AMQP</text>
<g>
<rect x="280" y="80" width="180" height="76" rx="12" fill="rgba(249,163,42,0.11)" stroke="#f9a32a" stroke-width="1.3" />
<rect x="298" y="96" width="144" height="24" rx="4" fill="none" stroke="#f0d4ac" stroke-width="1.2" />
<rect x="298" y="124" width="144" height="24" rx="4" fill="none" stroke="#f0d4ac" stroke-width="1.2" />
<text x="370" y="112" text-anchor="middle" fill="#2f69b3">FluxMQ</text>
<text x="370" y="140" text-anchor="middle" fill="#f9a32a">Durable Queue</text>
</g>
<g>
<rect x="280" y="230" width="180" height="76" rx="12" fill="rgba(249,163,42,0.11)" stroke="#f9a32a" stroke-width="1.3" />
<rect x="298" y="246" width="144" height="24" rx="4" fill="none" stroke="#f0d4ac" stroke-width="1.2" />
<rect x="298" y="274" width="144" height="24" rx="4" fill="none" stroke="#f0d4ac" stroke-width="1.2" />
<text x="370" y="262" text-anchor="middle" fill="#2f69b3">FluxMQ</text>
<text x="370" y="290" text-anchor="middle" fill="#f9a32a">Durable Queue</text>
</g>
<g>
<rect x="280" y="400" width="180" height="76" rx="12" fill="rgba(249,163,42,0.11)" stroke="#f9a32a" stroke-width="1.3" />
<rect x="298" y="416" width="144" height="24" rx="4" fill="none" stroke="#f0d4ac" stroke-width="1.2" />
<rect x="298" y="444" width="144" height="24" rx="4" fill="none" stroke="#f0d4ac" stroke-width="1.2" />
<text x="370" y="432" text-anchor="middle" fill="#2f69b3">FluxMQ</text>
<text x="370" y="460" text-anchor="middle" fill="#f9a32a">Durable Queue</text>
</g>
<rect x="620" y="118" width="112" height="44" rx="8" fill="rgba(42, 156, 74, 0.09)" stroke="#2a9c4a" stroke-width="1.3" />
<rect x="620" y="258" width="112" height="44" rx="8" fill="rgba(42, 156, 74, 0.09)" stroke="#2a9c4a" stroke-width="1.3" />
<rect x="620" y="398" width="112" height="44" rx="8" fill="rgba(42, 156, 74, 0.09)" stroke="#2a9c4a" stroke-width="1.3" />
<text x="676" y="146" text-anchor="middle" fill="#2a9c4a">Analytics</text>
<text x="676" y="286" text-anchor="middle" fill="#2a9c4a">Applications</text>
<text x="676" y="426" text-anchor="middle" fill="#2a9c4a">Services</text>
</g>
</svg>
</div>
@@ -0,0 +1,48 @@
---
import HeroNetwork from './HeroNetwork.astro';
---
<section class="relative h-[90vh] border-b-2 border-[var(--flux-border)] py-20 md:py-20">
<div class="mx-auto w-[min(100%-2.5rem,1200px)]">
<div class="grid items-center gap-12 md:grid-cols-2">
<div class="max-w-2xl">
<div class="mb-14">
<h1 class="animate-fade-in mb-4 text-6xl font-bold md:text-8xl" style="line-height:1.1">
<span class="text-[var(--flux-blue)]">Flux</span><span class="text-[var(--flux-orange)]">MQ</span>
</h1>
<div class="mono animate-slide-up mb-6 border-l-4 border-[var(--flux-orange)] pl-4 text-base text-theme-muted md:text-xl">
<div>open-source | high-throughput | low-latency | scalable</div>
<div>persistent storage | replication | MQTT v3/v5 | AMQP</div>
<div>security | extensibility</div>
</div>
</div>
<p class="animate-fade-in mb-14 max-w-3xl text-xl leading-relaxed md:text-2xl">
A high-performance, open source, multi-protocol message broker written in Go for scalable IoT and
event-driven architectures. Single binary. No external dependencies.
</p>
<div class="animate-slide-up flex flex-wrap gap-4">
<a
class="brutalist-border inline-block px-6 py-3 font-bold transition-colors hover:bg-[var(--flux-blue)] hover:text-white"
href="https://github.com/absmach/fluxmq"
target="_blank"
rel="noopener noreferrer"
>
VIEW ON GITHUB →
</a>
<a
class="brutalist-border inline-block px-6 py-3 font-bold transition-colors hover:bg-[var(--flux-orange)] hover:text-white"
href="/docs/"
>
READ DOCUMENTATION
</a>
</div>
</div>
<div class="hidden h-[80vh] items-center justify-center md:flex">
<HeroNetwork />
</div>
</div>
</div>
</section>
@@ -0,0 +1,37 @@
<section class="border-b-2 border-[var(--flux-border)] bg-[var(--flux-bg-alt)] py-20">
<div class="mx-auto w-[min(100%-2.5rem,1200px)]">
<div class="mx-auto max-w-[700px] text-center">
<h2 class="mb-2 text-3xl font-bold uppercase md:text-4xl">Subscribe to Newsletter</h2>
<p class="text-[var(--flux-muted)]">Stay updated with the latest FluxMQ news, updates, and announcements.</p>
<form
action="https://absmach.us11.list-manage.com/subscribe/post?u=70b43c7181d005024187bfb31&amp;id=0a319b6b63&amp;f_id=00d816e1f0"
method="post"
target="_blank"
class="mt-6 flex flex-col border-2 border-[var(--flux-border)] bg-white sm:flex-row"
>
<input
type="email"
name="EMAIL"
placeholder="Enter your email"
required
class="w-full flex-1 px-4 py-3 text-sm outline-none placeholder:text-[#6b6b6b] sm:text-base"
/>
<button
type="submit"
class="bg-[var(--flux-orange)] px-4 py-3 text-sm font-bold uppercase tracking-wide text-black transition hover:bg-[var(--flux-blue)] hover:text-white sm:text-base"
>
Subscribe
</button>
<input type="hidden" name="tags" value="8115284" />
</form>
<p class="mt-4 text-sm text-[var(--flux-muted)]">
By subscribing, you agree to our
<a class="underline" href="https://absmach.eu/privacy/" target="_blank" rel="noopener noreferrer">Privacy Policy</a>
and
<a class="underline" href="https://absmach.eu/terms/" target="_blank" rel="noopener noreferrer">Terms of Service</a>.
</p>
</div>
</div>
</section>
@@ -0,0 +1,49 @@
<section id="performance" class="border-b-2 border-theme bg-theme-alt py-20">
<div class="mx-auto w-[min(100%-2.5rem,1200px)]">
<h2 class="mb-12 text-4xl font-bold md:text-5xl">
<span class="border-l-4 border-[var(--flux-orange)] pl-4">PERFORMANCE</span>
</h2>
<div class="max-w-4xl">
<table class="metrics-table mono w-full text-sm md:text-base">
<thead>
<tr>
<th>METRIC</th>
<th>VALUE</th>
</tr>
</thead>
<tbody>
<tr>
<td>Concurrent Connections</td>
<td class="font-bold">500K+ per node</td>
</tr>
<tr>
<td>Message Throughput</td>
<td class="font-bold">300K-500K msg/s per node</td>
</tr>
<tr>
<td>Latency (local)</td>
<td class="font-bold">&lt;10ms</td>
</tr>
<tr>
<td>Latency (cross-node)</td>
<td class="font-bold">~5ms</td>
</tr>
<tr>
<td>Session Takeover</td>
<td class="font-bold">&lt;100ms</td>
</tr>
</tbody>
</table>
<div class="brutalist-border bg-theme mt-8 p-6">
<h3 class="mono mb-4 text-lg font-bold">CLUSTER SCALING</h3>
<ul class="mono space-y-2 text-sm">
<li class="border-l-4 border-[var(--flux-blue)] pl-4"><strong>3-node cluster:</strong> 1-2M msg/s</li>
<li class="border-l-4 border-[var(--flux-blue)] pl-4"><strong>5-node cluster:</strong> 2-4M msg/s</li>
<li class="border-l-4 border-[var(--flux-blue)] pl-4"><strong>Scaling:</strong> Linear with topic sharding</li>
</ul>
</div>
</div>
</div>
</section>
@@ -0,0 +1,84 @@
---
import CodePanel from './CodePanel.astro';
const dockerSnippet = `git clone https://github.com/absmach/fluxmq.git
cd fluxmq
docker compose -f docker/compose.yaml up -d`;
const mqttSnippet = `# Subscribe to all topics
mosquitto_sub -p 1883 -t "test/#" -v
# Publish a message
mosquitto_pub -p 1883 -t "test/hello" -m "Hello FluxMQ"`;
const localSnippet = `git clone https://github.com/absmach/fluxmq.git
cd fluxmq
make build
./build/fluxmq --config examples/no-cluster.yaml`;
---
<section id="quick-start" class="border-b-2 border-[var(--flux-border)] py-20">
<div class="mx-auto w-[min(100%-2.5rem,1200px)]">
<h2 class="mb-8 text-[clamp(2rem,4vw,3rem)] leading-tight font-bold uppercase tracking-[0.03em]">
<span class="border-l-[5px] border-[var(--flux-orange)] pl-3">Quick Start</span>
</h2>
<div class="space-y-8">
<div>
<h3 class="mono mb-3 text-xl font-bold">1. Run with Docker</h3>
<CodePanel code={dockerSnippet} />
</div>
<div>
<h3 class="mono mb-3 text-xl font-bold">2. Test with MQTT</h3>
<CodePanel code={mqttSnippet} />
</div>
<div>
<h3 class="mono mb-3 text-xl font-bold">3. Or Build Locally</h3>
<CodePanel code={localSnippet} />
</div>
<div class="cluster-box">
<p class="mono text-sm md:text-base"><strong>Defaults:</strong> MQTT TCP :1883, AMQP 0.9.1 :5682, Data /tmp/fluxmq/data</p>
<p class="mono mt-3"><strong>Next Steps:</strong></p>
<ul class="mt-2 list-none space-y-2 p-0">
<li>
<a
class="font-bold text-[var(--flux-blue)] hover:text-[var(--flux-orange)]"
href="https://github.com/absmach/fluxmq/tree/main/examples"
target="_blank"
rel="noopener noreferrer"
>
Explore code examples on GitHub
</a>
</li>
<li>
<a class="font-bold text-[var(--flux-blue)] hover:text-[var(--flux-orange)]" href="/docs/">
Read the full documentation
</a>
</li>
</ul>
</div>
</div>
</div>
<script is:inline>
const copyButtons = document.querySelectorAll('[data-copy-btn]');
for (const button of copyButtons) {
button.addEventListener('click', async () => {
const panel = button.closest('.code-panel');
const code = panel?.querySelector('code')?.textContent ?? '';
if (!code) return;
await navigator.clipboard.writeText(code);
const original = button.textContent;
button.textContent = 'Copied';
window.setTimeout(() => {
button.textContent = original;
}, 1600);
});
}
</script>
</section>
@@ -0,0 +1,61 @@
<section id="use-cases" class="border-b-2 border-[var(--flux-border)] py-20">
<div class="mx-auto w-[min(100%-2.5rem,1200px)]">
<h2 class="mb-12 text-4xl font-bold md:text-5xl">
<span class="border-l-4 border-[var(--flux-orange)] pl-4">USE CASES</span>
</h2>
<div class="grid gap-6 md:grid-cols-3">
<article class="brutalist-card p-6">
<div class="mb-4 inline-flex size-9 items-center justify-center rounded-sm border-2 border-[var(--flux-orange)] text-[var(--flux-orange)]">
<svg class="size-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 12h4l2-6 4 12 2-6h4"></path>
</svg>
</div>
<div class="mb-4 border-b-2 border-[var(--flux-border)] pb-4">
<h3 class="mono text-2xl font-bold">Event-Driven Systems</h3>
</div>
<ul class="space-y-3 text-base text-theme-muted">
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-orange)]">■</span><span>Decouple microservices with event streams</span></li>
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-orange)]">■</span><span>Reliable command and event pipelines (CQRS)</span></li>
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-orange)]">■</span><span>Background jobs and asynchronous workflows</span></li>
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-orange)]">■</span><span>Real-time data processing pipelines</span></li>
</ul>
</article>
<article class="brutalist-card p-6">
<div class="mb-4 inline-flex size-9 items-center justify-center rounded-sm border-2 border-[var(--flux-blue)] text-[var(--flux-blue)]">
<svg class="size-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="4" y="4" width="16" height="16" rx="2"></rect>
<path d="M9 9h6v6H9z"></path>
</svg>
</div>
<div class="mb-4 border-b-2 border-[var(--flux-border)] pb-4">
<h3 class="mono text-2xl font-bold">IoT & Real-Time</h3>
</div>
<ul class="space-y-3 text-base text-theme-muted">
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-orange)]">■</span><span>IoT device telemetry ingestion (MQTT)</span></li>
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-orange)]">■</span><span>Edge deployments with intermittent connectivity</span></li>
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-orange)]">■</span><span>Live dashboards and browser updates (WebSocket)</span></li>
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-orange)]">■</span><span>Constrained device messaging via protocol bridges</span></li>
</ul>
</article>
<article class="brutalist-card p-6">
<div class="mb-4 inline-flex size-9 items-center justify-center rounded-sm border-2 border-[var(--flux-orange)] text-[var(--flux-orange)]">
<svg class="size-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 18h16M6 18V9M12 18V5M18 18v-7"></path>
</svg>
</div>
<div class="mb-4 border-b-2 border-[var(--flux-border)] pb-4">
<h3 class="mono text-2xl font-bold">High-Throughput Pipelines</h3>
</div>
<ul class="space-y-3 text-base text-theme-muted">
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-orange)]">■</span><span>Stream millions of events per second</span></li>
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-orange)]">■</span><span>Buffer traffic bursts with durable queues</span></li>
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-orange)]">■</span><span>Decouple ingestion from downstream processing</span></li>
<li class="flex items-start"><span class="mr-2 font-bold text-[var(--flux-orange)]">■</span><span>Power analytics and observability data streams</span></li>
</ul>
</article>
</div>
</div>
</section>
+24
View File
@@ -0,0 +1,24 @@
---
import { ClientRouter } from 'astro:transitions';
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<ClientRouter />
<script is:inline>
(() => {
const stored = localStorage.getItem('theme');
const mode = stored === 'light' || stored === 'dark' ? stored : null;
const resolved =
mode ?? (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark');
document.documentElement.classList.toggle('dark', resolved === 'dark');
document.documentElement.dataset.theme = resolved;
})();
</script>
</head>
<body class="flex min-h-screen flex-col" transition:persist transition:animate="none" transition:name="page-layout">
<slot />
</body>
</html>
+48
View File
@@ -0,0 +1,48 @@
'use client';
import {
SearchDialog,
SearchDialogClose,
SearchDialogContent,
SearchDialogHeader,
SearchDialogIcon,
SearchDialogInput,
SearchDialogList,
SearchDialogOverlay,
type SharedProps,
} from 'fumadocs-ui/components/dialog/search';
import { useDocsSearch } from 'fumadocs-core/search/client';
import { create } from '@orama/orama';
import { useI18n } from 'fumadocs-ui/contexts/i18n';
function initOrama() {
return create({
schema: { _: 'string' },
language: 'english',
});
}
export default function DefaultSearchDialog(props: SharedProps) {
const { locale, locales } = useI18n();
const searchLocale = locales && locales.length > 0 ? locale : undefined;
const { search, setSearch, query } = useDocsSearch({
type: 'static',
initOrama,
locale: searchLocale,
});
return (
<SearchDialog search={search} onSearchChange={setSearch} isLoading={query.isLoading} {...props}>
<SearchDialogOverlay />
<SearchDialogContent>
<SearchDialogHeader>
<SearchDialogIcon />
<SearchDialogInput />
<SearchDialogClose />
</SearchDialogHeader>
<SearchDialogList items={query.data !== 'empty' ? query.data : null} />
</SearchDialogContent>
</SearchDialog>
);
}
+27
View File
@@ -0,0 +1,27 @@
import { glob } from 'astro/loaders';
import { defineCollection, z } from 'astro:content';
const docs = defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/docs/docs' }),
schema: z.object({
title: z.string(),
description: z.string().optional(),
icon: z.string().optional(),
}),
});
const meta = defineCollection({
loader: glob({ pattern: '**/*.{json,yaml}', base: './src/content/docs/docs' }),
schema: z.object({
title: z.string().optional(),
description: z.string().optional(),
pages: z.array(z.string()).optional(),
icon: z.string().optional(),
root: z.boolean().optional(),
}),
});
export const collections = {
docs,
meta,
};
@@ -0,0 +1,74 @@
---
title: FluxMQ Architecture
description: Comprehensive system design overview covering layered architecture, protocol adapters, domain logic, and multi-protocol support
---
# FluxMQ Architecture
**Last Updated:** 2026-02-05
## Overview
FluxMQ is a multi-protocol message broker built around a shared queue manager. MQTT transports (TCP, WebSocket, HTTP bridge, CoAP) share one MQTT broker instance, while AMQP 1.0 and AMQP 0.9.1 use dedicated brokers. Durable queues are protocol-agnostic and provide cross-protocol routing and fan-out.
## High-Level View
1. **Server wiring**
Initializes protocol servers (MQTT TCP/WS/HTTP/CoAP and AMQP), creates the shared queue manager, and wires storage, clustering, metrics, and graceful shutdown.
2. **Protocol entry points**
MQTT transports feed the MQTT broker, while AMQP 1.0 and AMQP 0.9.1 each feed their own protocol broker.
3. **Broker layer**
MQTT, AMQP 1.0, and AMQP 0.9.1 brokers process protocol semantics independently.
4. **Queue layer**
Queue-capable traffic from all brokers converges into the shared **Queue Manager** (durable logs, fan-out, consumer groups).
5. **Persistence layer**
Queue logs and related durable data are written through the storage backend.
`Server Wiring -> Protocol Servers -> Protocol Brokers -> Queue Manager -> Log Storage`
## Key Components (Code Map)
- **MQTT Broker**: `mqtt/broker/`
- Session lifecycle, topic routing, retained messages, wills
- Shared subscriptions (MQTT 5.0)
- Queue integration for `$queue/` topics and ack topics
- **AMQP Brokers**:
- AMQP 1.0: `amqp1/broker/`
- AMQP 0.9.1: `amqp/broker/`
- Both integrate with the shared queue manager
- **Transports and Bridges**: `server/`
- `server/tcp`, `server/websocket` for MQTT
- `server/http` HTTP publish bridge
- `server/coap` CoAP publish bridge
- `server/amqp`, `server/amqp1` for AMQP listeners
- **Queue Manager**: `queue/` and `logstorage/`
- Append-only logs with consumer groups
- Queue and stream modes
- Ack/Nack/Reject support and retention policies
- **Storage**: `storage/` (BadgerDB and memory backends)
- Sessions, subscriptions, retained messages, offline queues
- **Clustering**: `cluster/`
- Embedded etcd metadata, gRPC transport for routing
- Session ownership, retained/will coordination
- **Observability**: `server/otel/`
- OpenTelemetry metrics and tracing setup
- **Webhook Notifier**: `broker/webhook/`
- Asynchronous event delivery with retries and circuit breaker
- **Queue API (Connect/gRPC)**: `server/api/`, `server/queue/`
- Programmatic queue operations over HTTP/2 (h2c or TLS)
## Related Docs
- `docs/broker.md`
- `docs/queue.md`
- `docs/configuration.md`
- `docs/clustering.md`
- `docs/webhooks.md`
+65
View File
@@ -0,0 +1,65 @@
---
title: Broker & Message Routing
description: Internal MQTT broker architecture, session management, message routing mechanisms, topic matching, QoS handling, and cluster integration
---
# MQTT Broker Internals
**Last Updated:** 2026-02-05
This document describes the MQTT broker implementation and how it integrates with queues and clustering. The broker code lives in `mqtt/broker/` and uses shared components in `broker/` (router, webhook events, interfaces).
## Responsibilities
- Manage MQTT sessions (clean start, expiry, inflight tracking, offline queue)
- Route messages to local subscribers via the topic router
- Persist retained messages and wills
- Enforce QoS rules and MaxQoS downgrade
- Integrate with the queue manager for `$queue/` topics
- Integrate with clustering for cross-node routing and session takeover
- Emit webhook events (optional)
## Session Lifecycle (High Level)
- CONNECT arrives over a transport (TCP or WebSocket)
- Broker creates or resumes session based on `clean_start` and expiry
- Session state is restored from storage if needed
- In clustered mode, session ownership is acquired and takeover is handled
- On disconnect, offline queue is persisted (if session not expired)
## Message Routing
- Topic matching uses a trie-based router in `broker/router/`
- Shared subscriptions (MQTT 5.0) are handled by the shared subscription manager
- Retained messages are stored in the retained store and delivered on subscribe
- Queue topics (`$queue/...`) are routed to the queue manager
- Queue acks (`$queue/.../$ack|$nack|$reject`) are handled separately and do not enter normal pub/sub routing
## QoS Handling
- QoS 0, 1, and 2 are supported
- Inflight tracking is persisted for QoS 1/2 sessions
- MaxQoS is enforced by downgrading requested QoS when above server limits
## Cluster Integration
- Session ownership is coordinated via the cluster layer
- Publishes are routed to remote nodes with matching subscriptions
- Retained and will messages are backed by cluster-aware stores
- Session takeover is supported when a client reconnects to another node
## Optional Subsystems
- **Auth/Authz**: pluggable interfaces in `broker/auth.go`
- **Rate limiting**: per-IP and per-client limits in `ratelimit/`
- **Webhooks**: event delivery via `broker/webhook/`
- **OTel metrics/tracing**: optional, configured via `server` settings
## Configuration Pointers
- `broker.*` for broker limits (max message size, max QoS, retry policy)
- `session.*` for session storage and offline queue limits
- `ratelimit.*` for rate limiting
- `webhook.*` for webhook delivery
See `docs/configuration.md` for full details.
+636
View File
@@ -0,0 +1,636 @@
---
title: Client Library
description: Pure Go client libraries for MQTT 3.1.1/5.0 and AMQP 0.9.1 with durable queue support, auto-reconnect, and comprehensive features
---
# Client Library
Pure Go client libraries for MQTT 3.1.1/5.0 and AMQP 0.9.1 with durable queue support.
Note: FluxMQ also exposes AMQP 1.0 server support, but this repository does not
currently include a dedicated AMQP 1.0 client library.
---
## Features
- **Protocol Support:** MQTT 3.1.1 (v4) and MQTT 5.0 (v5)
- **Auto-Reconnect:** Exponential backoff with configurable limits
- **QoS Levels:** Full QoS 0/1/2 support with pluggable in-flight store (memory by default)
- **TLS/SSL:** Secure connections with custom certificates
- **Session Persistence:** Configurable session expiry
- **Durable Queues:** Consumer groups and acknowledgments (DLQ wiring pending)
- **MQTT 5.0 Features:** Topic aliases, user properties (publish/receive/will), flow control
---
## MQTT Client
## Quick Start
### Basic Connection
```go
package main
import (
"log"
"github.com/absmach/fluxmq/client"
)
func main() {
// Create client with options
opts := client.NewOptions().
SetServers("localhost:1883").
SetClientID("my-client").
SetProtocolVersion(5).
SetOnMessage(func(topic string, payload []byte, qos byte) {
log.Printf("Received: %s -> %s", topic, string(payload))
})
c := client.New(opts)
// Connect
if err := c.Connect(); err != nil {
log.Fatal(err)
}
defer c.Disconnect()
// Subscribe
if err := c.SubscribeSingle("sensors/#", 1); err != nil {
log.Fatal(err)
}
// Publish
if err := c.Publish("sensors/temp", []byte("22.5"), 1, false); err != nil {
log.Fatal(err)
}
// Keep running
select {}
}
```
---
## Configuration Options
### Connection Settings
```go
opts := client.NewOptions().
SetServers("broker1:1883", "broker2:1883"). // Multiple servers
SetClientID("device-001").
SetCredentials("user", "password").
SetTLSConfig(&tls.Config{...}). // Enable TLS
SetConnectTimeout(10 * time.Second).
SetKeepAlive(60 * time.Second)
```
### Protocol Version
```go
opts.SetProtocolVersion(4) // MQTT 3.1.1
opts.SetProtocolVersion(5) // MQTT 5.0
```
### Session Options
```go
opts.SetCleanSession(true) // Start fresh each connect
opts.SetCleanSession(false) // Resume previous session
opts.SetSessionExpiry(3600) // Session persists 1 hour after disconnect (v5)
```
### MQTT 5.0 Specific
```go
opts.SetSessionExpiry(86400). // Session expires in 24h
SetReceiveMaximum(100). // Max inflight messages to receive
SetMaximumPacketSize(1048576). // Max 1MB packets
SetTopicAliasMaximum(10). // Enable topic aliases
SetRequestResponseInfo(true). // Request response info
SetRequestProblemInfo(true) // Get detailed errors
```
### Reconnection
```go
opts.SetAutoReconnect(true) // Enable auto-reconnect
opts.ReconnectBackoff = 1 * time.Second // Initial delay
opts.MaxReconnectWait = 2 * time.Minute // Max delay
```
---
## Publishing Messages
### Basic Publish
```go
// QoS 0 - Fire and forget
c.Publish("topic", []byte("payload"), 0, false)
// QoS 1 - At least once
c.Publish("topic", []byte("payload"), 1, false)
// QoS 2 - Exactly once
c.Publish("topic", []byte("payload"), 2, false)
// Retained message
c.Publish("config/device", []byte("settings"), 1, true)
```
### MQTT 5.0 Publish Properties
Use `PublishMessage` to set publish properties such as content type, response
topic, correlation data, and user properties (MQTT 5.0 only).
```go
msg := &client.Message{
Topic: "sensors/temp",
Payload: []byte("22.5"),
QoS: 1,
ContentType: "text/plain",
ResponseTopic: "responses/temp",
CorrelationData: []byte("req-123"),
UserProperties: map[string]string{"unit": "celsius"},
}
if err := c.PublishMessage(msg); err != nil {
log.Fatal(err)
}
```
---
## Subscribing to Topics
### Basic Subscription
```go
// Single topic
c.SubscribeSingle("sensors/temp", 1)
// Multiple topics
c.Subscribe(map[string]byte{
"sensors/#": 1,
"devices/+/status": 2,
})
```
### MQTT 5.0 Subscription Options
```go
opts := &client.SubscribeOption{
Topic: "sensors/temp",
QoS: 1,
NoLocal: true, // Don't receive own messages
RetainAsPublished: true, // Keep original retain flag
RetainHandling: 1, // Only send retained if new sub
SubscriptionID: 42, // Track subscription
}
c.SubscribeWithOptions(opts)
```
### Unsubscribe
```go
c.Unsubscribe("sensors/temp")
```
---
## Message Handling
### Simple Handler
```go
opts.SetOnMessage(func(topic string, payload []byte, qos byte) {
log.Printf("[%s] QoS %d: %s", topic, qos, payload)
})
```
### Full Message Context (MQTT 5.0)
```go
opts.SetOnMessageV2(func(msg *client.Message) {
log.Printf("Topic: %s", msg.Topic)
log.Printf("Payload: %s", msg.Payload)
log.Printf("QoS: %d", msg.QoS)
log.Printf("Retain: %v", msg.Retain)
log.Printf("Properties: %+v", msg.UserProperties)
log.Printf("Response Topic: %s", msg.ResponseTopic)
})
```
---
## Durable Queues
The client supports durable queues with consumer groups and message acknowledgment.
Reject/DLQ wiring in the broker is pending.
MQTT v3 can publish and subscribe to queue topics, but acknowledgments require MQTT v5 user properties.
**When to use queues instead of regular pub/sub:**
- You need at-least-once processing with explicit acknowledgments
- Multiple consumers should share the workload (consumer groups)
- Failed messages need retry logic or dead-letter handling
**Key concepts:**
- **Queue**: Persistent message buffer with ordered delivery per queue (single log)
- **Consumer Group**: Multiple consumers share messages from the same queue
- **Acknowledgment**: Confirm success (Ack), request redelivery (Nack), or reject permanently (Reject)
### Publishing to Queues
```go
// Simple queue publish
c.PublishToQueue("orders", []byte(`{"item": "widget"}`))
// Full control
c.PublishToQueueWithOptions(&client.QueuePublishOptions{
QueueName: "events",
Payload: []byte("event-data"),
Properties: map[string]string{"priority": "high"},
QoS: 1,
})
```
### Subscribing to Queues
```go
// Subscribe with consumer group
err := c.SubscribeToQueue("orders", "order-processors", func(msg *client.QueueMessage) {
log.Printf("Processing order: %s", msg.Payload)
log.Printf("Message ID: %s", msg.MessageID)
log.Printf("Group: %s", msg.GroupID)
log.Printf("Offset: %d", msg.Offset)
// Process message...
if processedOK {
msg.Ack() // Message removed from queue
} else if shouldRetry {
msg.Nack() // Redelivery eligible (subject to broker delivery/visibility timing)
} else {
msg.Reject() // Removes from pending; DLQ routing not wired yet
}
})
```
### Message Acknowledgment
| Method | Effect |
| -------------- | -------------------------------------------------- |
| `msg.Ack()` | Message processed successfully, removed from queue |
| `msg.Nack()` | Processing failed, make eligible for redelivery |
| `msg.Reject()` | Remove from pending; DLQ routing not wired yet |
### Direct Acknowledgment
```go
// Acknowledge by message ID (explicit group)
c.AckWithGroup("orders", "msg-12345", "processors")
c.NackWithGroup("orders", "msg-12345", "processors")
c.RejectWithGroup("orders", "msg-12345", "processors")
```
Note: MQTT queue acknowledgments require MQTT v5 and the broker expects
`message-id` and `group-id` user properties on ack messages. `QueueMessage.Ack()`
sends both when they are present on incoming messages.
### Unsubscribe from Queue
```go
c.UnsubscribeFromQueue("orders")
```
### Queue Code Example
```go
package main
import (
"log"
"github.com/absmach/fluxmq/client"
)
func main() {
opts := client.NewOptions().
SetServers("localhost:1883").
SetClientID("order-processor").
SetProtocolVersion(5)
c := client.New(opts)
if err := c.Connect(); err != nil {
log.Fatal(err)
}
defer c.Disconnect()
// Subscribe to order queue with consumer group
err := c.SubscribeToQueue("orders", "processors", func(msg *client.QueueMessage) {
log.Printf("Order received: %s", msg.Payload)
// Simulate processing
if processOrder(msg.Payload) {
if err := msg.Ack(); err != nil {
log.Printf("Ack failed: %v", err)
}
} else {
msg.Nack() // Retry later
}
})
if err != nil {
log.Fatal(err)
}
// Publish some orders
for i := 0; i < 10; i++ {
c.PublishToQueue("orders", []byte(`{"id": "`+string(rune(i))+`"}`))
}
select {} // Keep running
}
func processOrder(payload []byte) bool {
// Process order logic
return true
}
```
---
## Connection Lifecycle
### Callbacks
```go
opts.SetOnConnect(func() {
log.Println("Connected!")
}).
SetOnConnectionLost(func(err error) {
log.Printf("Connection lost: %v", err)
}).
SetOnReconnecting(func(attempt int) {
log.Printf("Reconnecting (attempt %d)...", attempt)
}).
SetOnServerCapabilities(func(caps *client.ServerCapabilities) {
log.Printf("Server max QoS: %d", caps.MaximumQoS)
log.Printf("Server retain available: %v", caps.RetainAvailable)
})
```
### Disconnect
```go
// Normal disconnect
c.Disconnect()
// With reason (MQTT 5.0)
c.DisconnectWithReason(0x04, 0, "Going offline")
```
---
## Will Messages
Configure a last-will message sent when the client disconnects unexpectedly:
```go
opts.SetWill("clients/device-001/status", []byte("offline"), 1, true)
```
### With MQTT 5.0 Properties
```go
opts.Will = &client.WillMessage{
Topic: "clients/device-001/status",
Payload: []byte("offline"),
QoS: 1,
Retain: true,
WillDelayInterval: 30, // Wait 30s before sending
MessageExpiry: 3600,
UserProperties: map[string]string{"reason": "unexpected"},
}
```
---
## Error Handling
### Common Errors
| Error | Cause |
| ------------------------- | ------------------------------------------ |
| `ErrNotConnected` | Operation attempted while disconnected |
| `ErrNoServers` | No broker addresses configured |
| `ErrEmptyClientID` | ClientID not set |
| `ErrInvalidProtocol` | Protocol version must be 4 or 5 |
| `ErrInvalidQoS` | QoS must be 0, 1, or 2 |
| `ErrInvalidTopic` | Empty or invalid topic string |
| `ErrInvalidMessage` | Message is nil or invalid |
| `ErrMaxInflight` | Too many pending messages |
| `ErrQueueAckRequiresV5` | Queue acks require MQTT v5 user properties |
| `ErrQueueAckMissingGroup` | `group-id` missing for queue ack |
### Handling Connection Errors
```go
if err := c.Connect(); err != nil {
switch err {
case client.ErrNoServers:
log.Fatal("No brokers configured")
case client.ErrEmptyClientID:
log.Fatal("ClientID required")
default:
log.Printf("Connection error: %v", err)
}
}
```
---
## Message Store
For QoS 1/2 in-flight storage:
```go
store := client.NewMemoryStore()
opts.SetStore(store)
```
Built-in stores:
- **MemoryStore** (default): In-memory, lost on restart
You can implement the `MessageStore` interface to persist QoS 1/2 in-flight data.
---
## Defaults
| Option | Default Value |
| ---------------- | -------------- |
| KeepAlive | 60 seconds |
| ConnectTimeout | 10 seconds |
| WriteTimeout | 5 seconds |
| AckTimeout | 10 seconds |
| PingTimeout | 5 seconds |
| MaxInflight | 100 |
| MessageChanSize | 256 |
| AutoReconnect | true |
| ReconnectBackoff | 1 second |
| MaxReconnectWait | 2 minutes |
| ProtocolVersion | 4 (MQTT 3.1.1) |
| CleanSession | true |
---
## AMQP 0.9.1 Client
The AMQP 0.9.1 client focuses on durable queue interop with the broker. It uses the same queue naming convention as MQTT: pass the queue name without the `$queue/` prefix.
### Quick Start
```go
package main
import (
"log"
"github.com/absmach/fluxmq/client/amqp"
)
func main() {
opts := amqp.NewOptions().
SetAddress("localhost:5682").
SetCredentials("guest", "guest")
c, err := amqp.New(opts)
if err != nil {
log.Fatal(err)
}
if err := c.Connect(); err != nil {
log.Fatal(err)
}
defer c.Close()
// Subscribe to a queue with a consumer group
err = c.SubscribeToQueue("tasks/orders", "order-shipper", func(msg *amqp.QueueMessage) {
log.Printf("Received: %s", string(msg.Body))
_ = msg.Ack()
})
if err != nil {
log.Fatal(err)
}
// Publish to the same queue
if err := c.PublishToQueue("tasks/orders", []byte("hello")); err != nil {
log.Fatal(err)
}
select {}
}
```
### Queue Semantics
- `SubscribeToQueue` passes the consumer group via `x-consumer-group` on `basic.consume`.
- `Ack`, `Nack`, and `Reject` map to `basic.ack`, `basic.nack`, and `basic.reject`.
### Stream Queues (RabbitMQ-Compatible)
Stream queues provide log-style consumption with cursor offsets.
Stream queue names follow RabbitMQ conventions (no `$queue/` prefix).
Offsets are passed as `x-stream-offset` strings; values like `first`, `last`,
`next`, `offset=<n>`, `timestamp=<unix>` are interpreted by the broker.
```go
// Declare a stream queue
qName, err := c.DeclareStreamQueue(&amqp.StreamQueueOptions{
Name: "events",
Durable: true,
MaxAge: "7D",
MaxLengthBytes: 10 * 1024 * 1024 * 1024,
})
if err != nil {
log.Fatal(err)
}
log.Printf("stream queue: %s", qName)
// Consume from the beginning
err = c.SubscribeToStream(&amqp.StreamConsumeOptions{
QueueName: "events",
Offset: "first",
}, func(msg *amqp.QueueMessage) {
if off, ok := msg.StreamOffset(); ok {
log.Printf("offset=%d payload=%s", off, string(msg.Body))
}
_ = msg.Ack()
})
if err != nil {
log.Fatal(err)
}
// Publish to the stream queue (RabbitMQ-style)
if err := c.PublishToStream("events", []byte("hello"), nil); err != nil {
log.Fatal(err)
}
```
Stream deliveries include:
- `x-stream-offset`
- `x-stream-timestamp`
- `x-work-acked` / `x-work-committed-offset`
The `x-work-*` fields report the configured primary work group's committed offset.
`x-work-acked` is `true` when this message's offset is below the committed offset,
which can lag slightly due to auto-commit interval batching.
Convenience accessors are available on `QueueMessage`:
`StreamOffset()`, `StreamTimestamp()`, `WorkAcked()`, `WorkCommittedOffset()`, `WorkGroup()`.
### Manual Commit Mode
By default, stream consumers auto-commit offsets as messages are delivered
(similar to Kafka's `enable.auto.commit=true`). For exactly-once processing,
disable auto-commit and commit explicitly.
Auto-commit is rate-limited by the server setting
`queue_manager.auto_commit_interval` (default: `5s`).
Minimal example:
```go
autoCommit := false
_ = c.SubscribeToStream(&amqp.StreamConsumeOptions{
QueueName: "events",
ConsumerGroup: "my-group",
AutoCommit: &autoCommit,
}, handler)
_ = c.CommitOffset("events", "my-group", lastProcessedOffset)
```
Use the same consumer group name in both calls.
With manual commit:
- Messages are delivered but the committed offset doesn't advance automatically
- On reconnect, delivery resumes from the last committed offset
- Use `CommitOffset()` to advance the committed position
### Pub/Sub
```go
_ = c.Subscribe("sensors/#", func(msg *amqp.Message) {
log.Printf("Topic: %s Payload: %s", msg.Topic, string(msg.Body))
})
_ = c.Publish("sensors/temp", []byte("22.5"))
```
### Reconnection
```go
opts.SetAutoReconnect(true).
SetReconnectBackoff(1 * time.Second).
SetMaxReconnectWait(2 * time.Minute).
SetOnConnectionLost(func(err error) { log.Printf("lost: %v", err) }).
SetOnReconnecting(func(attempt int) { log.Printf("reconnect attempt %d", attempt) })
```
@@ -0,0 +1,60 @@
---
title: Clustering
description: Distributed broker clustering with embedded etcd, gRPC transport, session takeover, and high availability architecture
---
# Clustering
**Last Updated:** 2026-02-05
FluxMQ supports optional clustering for high availability and cross-node routing. Clustering is embedded and uses etcd for metadata coordination plus gRPC for inter-broker transport.
## What Clustering Provides
- **Session ownership** across nodes
- **Subscription routing** for cross-node publishes
- **Queue consumer registry** for cross-node queue delivery
- **Retained/will storage** with a hybrid metadata + payload strategy
- **Session takeover** when a client reconnects to a different node
## Core Components
- **Embedded etcd**: stores session ownership, subscriptions, queue consumers, retained metadata
- **Transport (gRPC)**: routes publishes, queue deliveries, retained/will fetches
- **Optional Raft for queues**: configurable replication for queue appends
## Configuration (Minimal)
```yaml
cluster:
enabled: true
node_id: "broker-1"
etcd:
data_dir: "/tmp/fluxmq/etcd"
bind_addr: "0.0.0.0:2380"
client_addr: "0.0.0.0:2379"
initial_cluster: "broker-1=http://0.0.0.0:2380"
bootstrap: true
transport:
bind_addr: "0.0.0.0:7948"
peers: {}
```
For full cluster options (TLS, Raft, hybrid retained settings), see `docs/configuration.md`.
## Message Routing (High Level)
- On publish, the broker routes to local subscribers and calls the cluster router to forward to remote nodes with matching subscriptions.
- Retained and will messages are stored in a cluster-aware store with metadata in etcd and payloads stored locally for larger messages.
- When a client reconnects to a different node, the new node requests session takeover from the previous owner.
## Queue Delivery Across Nodes
Queue consumers are registered in the cluster. When a queue publish occurs, the queue manager determines which nodes host matching consumers and forwards delivery to those nodes.
## Notes
- Clustering is optional; single-node mode uses in-memory or BadgerDB storage.
- Queue replication is optional and controlled via `cluster.raft`.
@@ -0,0 +1,58 @@
---
title: Comparison
description: Comprehensive comparison of FluxMQ against industry solutions including EMQX, HiveMQ, Artemis, Mosquitto, NATS, RabbitMQ, and Kafka
---
# Comparison Guide
**Last Updated:** 2026-02-05
This document provides a basic, evergreen comparison guide without hard claims about thirdparty products. Vendor feature sets change frequently; verify details with official docs before making a decision.
## FluxMQ Snapshot (from current codebase)
- **Protocols**: MQTT 3.1.1/5.0 (TCP, WebSocket), HTTP publish bridge, CoAP publish bridge, AMQP 1.0 and AMQP 0.9.1
- **Queues**: Durable queues, consumer groups, ack/nack/reject; stream queues supported; retention policies (time/size/message count)
- **DLQ**: Handler exists, automatic DLQ routing not wired yet
- **Clustering**: Embedded etcd coordination, gRPC routing, session takeover, hybrid retained storage, optional Raft for queue appends
- **Storage**: BadgerDB or inmemory; pluggable interfaces
- **Security**: TLS/mTLS, WebSocket origin validation, rate limiting
- **Observability**: OpenTelemetry metrics/tracing (OTLP); no native Prometheus endpoint
- **Clients**: Go MQTT client, Go AMQP 0.9.1 client (AMQP 1.0 client not provided)
## How to Compare Systems (by category)
### MQTTNative Brokers
Best fit when:
- MQTT 5.0 features and device connectivity are the primary focus
- Edge/IoT workloads need lightweight clients and topicbased routing
### QueueCentric Brokers
Best fit when:
- Workqueue semantics and explicit acknowledgments are primary
- Finegrained retry/DLQ handling is critical
### LogCentric Brokers
Best fit when:
- Longterm event retention and replay are primary
- Stream processing and analytics require strong log semantics
### MultiProtocol Brokers
Best fit when:
- You need multiple client protocols in a single deployment
- Crossprotocol routing and shared durability are important
## Decision Checklist
- Protocols you must support (MQTT, AMQP, HTTP, CoAP)
- Delivery guarantees required (QoS 0/1/2, atleastonce, exactlyonce)
- Durability and retention needs (time/size retention, replay)
- Operational complexity you can tolerate (single binary vs external dependencies)
- Cluster behavior (session takeover, routing, replication strategy)
- Observability requirements (OTLP, Prometheus, tracing)
- Client library availability and ecosystem fit
## Notes
- FluxMQ is still evolving. For current status of features and limitations, see `docs/queue.md`, `docs/configuration.md`, and `docs/roadmap.md`.
- Benchmarking should be done on your target hardware using `benchmarks/`.
@@ -0,0 +1,285 @@
---
title: Configuration Guide
description: Comprehensive configuration reference for server transports, broker settings, clustering, storage, security, and operational features
---
# Configuration Guide
**Last Updated:** 2026-02-05
FluxMQ uses a single YAML configuration file. Start the broker with:
```bash
./build/fluxmq --config /path/to/config.yaml
```
If `--config` is omitted, defaults are used (see `config.Default()` in `config/config.go`).
## Configuration Overview
Top-level keys:
- `server`
- `broker`
- `session`
- `queue_manager`
- `queues`
- `storage`
- `cluster`
- `webhook`
- `ratelimit`
- `log`
Durations use Go duration strings like `5s`, `1m`, `24h`.
## Server
`server` controls network listeners and telemetry endpoints.
```yaml
server:
tcp:
plain:
addr: ":1883"
max_connections: 10000
read_timeout: "60s"
write_timeout: "60s"
tls: {}
mtls: {}
websocket:
plain:
addr: ":8083"
path: "/mqtt"
allowed_origins: ["https://app.example.com"]
tls: {}
mtls: {}
http:
plain:
addr: ":8080"
tls: {}
mtls: {}
coap:
plain:
addr: ":5683"
dtls: {}
mdtls: {}
amqp:
plain:
addr: ":5672"
tls: {}
mtls: {}
amqp091:
plain:
addr: ":5682"
tls: {}
mtls: {}
health_enabled: true
health_addr: ":8081"
metrics_enabled: false
metrics_addr: "localhost:4317" # OTLP endpoint
otel_service_name: "fluxmq"
otel_service_version: "dev"
otel_metrics_enabled: true
otel_traces_enabled: false
otel_trace_sample_rate: 0.1
api_enabled: false
api_addr: ":9090" # Queue API (Connect/gRPC)
shutdown_timeout: "30s"
```
### TLS/DTLS Settings
TLS fields are shared across `tls`, `mtls`, `dtls`, and `mdtls` blocks via `pkg/tls` config:
- `cert_file`, `key_file`
- `ca_file` (client CA) and `server_ca_file`
- `client_auth` (e.g., `require`, `verify_if_given`)
- `min_version`, `cipher_suites`, `prefer_server_cipher_suites`
- `ocsp`, `crl` (advanced verification)
## Broker
```yaml
broker:
max_message_size: 1048576
max_retained_messages: 10000
retry_interval: "20s"
max_retries: 0
max_qos: 2
```
## Session
```yaml
session:
max_sessions: 10000
default_expiry_interval: 300
max_offline_queue_size: 1000
max_inflight_messages: 100
offline_queue_policy: "evict" # evict or reject
```
## Queue Manager
```yaml
queue_manager:
auto_commit_interval: "5s" # Stream groups auto-commit cadence
```
## Queues
Queue configuration controls durable queues and stream queues.
```yaml
queues:
- name: "mqtt"
topics: ["$queue/#"]
reserved: true
type: "classic" # classic or stream
primary_group: "" # stream status reporting
limits:
max_message_size: 10485760
max_depth: 100000
message_ttl: "168h"
retry:
max_retries: 10
initial_backoff: "5s"
max_backoff: "5m"
multiplier: 2.0
dlq:
enabled: true
topic: "" # optional override
retention:
max_age: "168h"
max_length_bytes: 0
max_length_messages: 0
```
## Storage
```yaml
storage:
type: "badger" # memory or badger
badger_dir: "/tmp/fluxmq/data"
sync_writes: false
```
## Cluster
```yaml
cluster:
enabled: false
node_id: "broker-1"
etcd:
data_dir: "/tmp/fluxmq/etcd"
bind_addr: "0.0.0.0:2380"
client_addr: "0.0.0.0:2379"
initial_cluster: "broker-1=http://0.0.0.0:2380"
bootstrap: true
hybrid_retained_size_threshold: 1024
transport:
bind_addr: "0.0.0.0:7948"
peers: {}
tls_enabled: false
tls_cert_file: ""
tls_key_file: ""
tls_ca_file: ""
raft:
enabled: false
replication_factor: 3
sync_mode: true
min_in_sync_replicas: 2
ack_timeout: "5s"
write_policy: "forward" # local, reject, forward
distribution_mode: "replicate" # forward, replicate
bind_addr: "127.0.0.1:7100"
data_dir: "/tmp/fluxmq/raft"
peers: {}
heartbeat_timeout: "1s"
election_timeout: "3s"
snapshot_interval: "5m"
snapshot_threshold: 8192
```
## Webhooks
```yaml
webhook:
enabled: false
queue_size: 10000
drop_policy: "oldest" # oldest or newest
workers: 5
include_payload: false
shutdown_timeout: "30s"
defaults:
timeout: "5s"
retry:
max_attempts: 3
initial_interval: "1s"
max_interval: "30s"
multiplier: 2.0
circuit_breaker:
failure_threshold: 5
reset_timeout: "60s"
endpoints:
- name: "analytics"
type: "http"
url: "https://example.com/webhook"
events: ["message.published"]
topic_filters: ["sensors/#"]
headers:
Authorization: "Bearer token"
timeout: "10s"
```
Only `http` endpoints are supported at the moment.
## Rate Limiting
```yaml
ratelimit:
enabled: false
connection:
enabled: true
rate: 1.6667 # connections per second per IP
burst: 20
cleanup_interval: "5m"
message:
enabled: true
rate: 1000 # messages per second per client
burst: 100
subscribe:
enabled: true
rate: 100 # subscriptions per second per client
burst: 10
```
## Logging
```yaml
log:
level: "info" # debug, info, warn, error
format: "text" # text or json
```
+124
View File
@@ -0,0 +1,124 @@
---
title: FluxMQ Documentation
description: Welcome to FluxMQ - a high-performance, multi-protocol message broker with MQTT, WebSocket, HTTP, and CoAP support
---
# FluxMQ
A high-performance, multi-protocol message broker written in Go designed for scalability, extensibility, and protocol diversity. Supports MQTT 3.1.1 and 5.0 over TCP and WebSocket, plus HTTP-MQTT and CoAP bridges for IoT integration.
## Features
- **Multi-Protocol Support** - MQTT 3.1.1/5.0, WebSocket, HTTP-MQTT Bridge, CoAP Bridge
- **High Performance** - 300K-500K msg/s per node with zero-copy packet parsing
- **Clustering** - Embedded etcd for distributed coordination, no external dependencies
- **Durable Queues** - Consumer groups with ack/nack/reject semantics
- **Security** - TLS/mTLS, rate limiting, authentication plugins
- **Observability** - OpenTelemetry metrics, structured logging, webhook notifications
## Quick Start
### Build & Run
```bash
# Clone and build
git clone https://github.com/absmach/fluxmq.git
cd fluxmq
make build
# Run single node
./build/fluxmq
# Run with configuration
./build/fluxmq --config config.yaml
```
### Test with MQTT Client
```bash
# Subscribe
mosquitto_sub -p 1883 -t "test/#" -v
# Publish
mosquitto_pub -p 1883 -t "test/hello" -m "Hello FluxMQ"
```
### Basic Configuration
```yaml
server:
tcp:
plain:
addr: ":1883"
websocket:
plain:
addr: ":8083"
path: "/mqtt"
http:
plain:
addr: ":8080"
broker:
max_message_size: 1048576
max_retained_messages: 10000
storage:
type: badger
badger_dir: "./data"
log:
level: info
```
## Architecture Overview
FluxMQ uses a clean layered architecture:
- **Transport Layer** - TCP, WebSocket, HTTP, CoAP servers
- **Protocol Layer** - MQTT 3.1.1/5.0 packet handling
- **Domain Layer** - Protocol-agnostic broker logic
- **Storage Layer** - BadgerDB for persistence, etcd for clustering
## Documentation
- [Architecture](/docs/architecture): System design and component overview
- [Configuration](/docs/configuration): Complete configuration reference
- [Clustering](/docs/clustering): Distributed broker setup
- [Client Libraries](/docs/client): Go MQTT and AMQP clients
- [Durable Queues](/docs/queue): Queue system with consumer groups
- [Webhooks](/docs/webhooks): Event notification system
## Performance
| Metric | Value |
| -------------------------- | ------------------------ |
| **Concurrent Connections** | 500K+ per node |
| **Message Throughput** | 300K-500K msg/s per node |
| **Latency (local)** | \<10ms |
| **Session Takeover** | \<100ms |
## Use Cases
**Event-Driven Architectures**
- Event backbone for microservices
- CQRS systems with durable queues
- Real-time event processing
**IoT & Real-Time Systems**
- Device communication via MQTT
- Browser clients via WebSocket
- Edge computing deployments
**High-Availability Systems**
- Clustered deployments (3-5 nodes)
- Geographic distribution
- Linear scaling
## Getting Help
- **GitHub**: [github.com/absmach/fluxmq](https://github.com/absmach/fluxmq)
- **Issues**: Report bugs and request features
- **Discussions**: Community support and questions
+20
View File
@@ -0,0 +1,20 @@
{
"root": true,
"pages": [
"---Overview---",
"index",
"roadmap",
"---Core Concepts---",
"architecture",
"broker",
"queue",
"clustering",
"---Operations---",
"configuration",
"scaling",
"webhooks",
"---Reference---",
"client",
"competition"
]
}
+176
View File
@@ -0,0 +1,176 @@
---
title: Durable Queues
description: Shared queue system for MQTT and AMQP with consumer groups, acknowledgments, stream queues, and append-only log storage
---
# Durable Queues
**Last Updated:** 2026-02-05
FluxMQ provides durable queues shared across MQTT, AMQP 1.0, and AMQP 0.9.1. The queue manager is append-only with consumer groups and supports both classic work-queue semantics and stream-style consumption.
## Overview
- **Queue topics** use the `$queue/` prefix
- **Consumer groups** enable load-balanced processing
- **Ack/Nack/Reject** are supported across protocols
- **Retention** can be configured per queue (time/size/message count)
- **Stream queues** are supported via queue type `stream`
- **DLQ handler exists**, but automatic DLQ routing is not wired yet
## Architecture
```
┌──────────────┐ ┌──────────────┐ ┌───────────────┐
│ MQTT Broker │ │ AMQP Broker │ │ AMQP091 Broker│
│ (TCP/WS/ │ │ (AMQP 1.0) │ │ (AMQP 0.9.1) │
│ HTTP/CoAP) │ │ │ │ │
└──────┬───────┘ └───────┬──────┘ └──────┬────────┘
│ │ │
└──────────────────┼────────────────┘
┌─────────────────────────┐
│ Shared Queue Manager │
│ - Topic bindings │
│ - Consumer groups │
│ - Retention loop │
└───────────┬─────────────┘
┌─────────────────────────┐
│ Log Storage (AOL) │
└─────────────────────────┘
```
## Queue Addressing
Queue topics use `$queue/<queue-name>/...`.
Examples:
- Publish: `$queue/orders`
- Subscribe to a pattern: `$queue/orders/#`
- Ack: `$queue/orders/$ack`
- Nack: `$queue/orders/$nack`
- Reject: `$queue/orders/$reject`
## Consumer Groups
- **MQTT v5**: provide `consumer-group` as a user property on SUBSCRIBE
- **MQTT v3**: consumer group falls back to client ID (acks require MQTT v5)
- **AMQP 1.0**: provide `consumer-group` in attach properties
- **AMQP 0.9.1**: provide `x-consumer-group` in `basic.consume`
## Message Properties
Queue deliveries include these properties:
- `message-id` (required for ack/nack/reject)
- `group-id` (consumer group name)
- `queue` (queue name)
- `offset` (sequence number)
Stream deliveries also include:
- `x-stream-offset`
- `x-stream-timestamp` (unix millis)
- `x-work-committed-offset` (if primary group is configured)
- `x-work-acked` (true when below committed offset)
- `x-work-group` (primary work group name)
## Acknowledgments
### MQTT
Ack/Nack/Reject are implemented by publishing to `$queue/<queue>/$ack|$nack|$reject` with MQTT v5 user properties:
- `message-id`
- `group-id`
MQTT v3 can publish and subscribe to queue topics, but acknowledgments require MQTT v5 user properties.
### AMQP 1.0
AMQP dispositions are mapped to queue acknowledgments:
- Accepted → Ack
- Released → Nack
- Rejected → Reject
### AMQP 0.9.1
- `basic.ack`, `basic.nack`, `basic.reject` map to Ack/Nack/Reject
#### Stream Commit (AMQP 0.9.1)
Stream consumers can explicitly commit offsets by publishing to:
- `$queue/<queue>/$commit`
Headers:
- `x-group-id`
- `x-offset`
## Queue Types
### Classic (Work Queue)
- Ack/Nack/Reject semantics
- Pending entry tracking per consumer group
- Redelivery uses visibility timeouts and work stealing
- Retry backoff settings are accepted in config but not yet enforced in delivery timing
### Stream
- Append-only log semantics
- Cursor-based consumption
- Optional manual commit
## Retention
Retention policies can be configured per queue:
- `max_age` (time-based)
- `max_length_bytes`
- `max_length_messages`
A background retention loop truncates logs to the safe offset based on configured limits.
## DLQ Status
A DLQ handler exists in `queue/consumer/dlq.go`, but the main delivery path does not automatically move rejected or expired messages into a DLQ yet. `Reject` currently removes the message from the pending list without pushing it to a DLQ.
## Configuration
Queues are configured under `queues` in the main config file:
```yaml
queue_manager:
auto_commit_interval: "5s"
queues:
- name: "orders"
topics: ["$queue/orders/#"]
type: "classic" # classic or stream
primary_group: "" # optional for stream status
limits:
max_message_size: 10485760
max_depth: 100000
message_ttl: "168h"
retry:
max_retries: 10
initial_backoff: "5s"
max_backoff: "5m"
multiplier: 2.0
dlq:
enabled: true
topic: "" # optional override
retention:
max_age: "168h"
max_length_bytes: 0
max_length_messages: 0
```
@@ -0,0 +1,26 @@
---
title: Development Roadmap
description: Production-ready MQTT broker development roadmap covering queue architecture, security hardening, performance optimization, and clustering
---
# Roadmap
**Last Updated:** 2026-02-05
*This project is under heavy development and many important features may evolve and change as development progresses.*
This roadmap highlights nearterm focus areas. Ordering may change as issues are discovered and priorities shift.
## Focus Areas
- Tests
- Benchmarks
- Optimizations
- Architecture revision after the above
- Scaling and recovery tests
- Performance optimization and code cleanup
- Dashboards and a basic UI with metrics
- Improved and faster logging and telemetry
- Extensive storage tests
For daytoday progress, track open issues and PRs.
@@ -0,0 +1,38 @@
---
title: Scaling & Performance
description: Comprehensive scaling guide covering capacity analysis, performance optimizations, benchmarks, topic sharding, and architecture for 100M+ clients
---
# Scaling & Performance
**Last Updated:** 2026-02-05
Performance and scaling are workload-dependent. Use the benchmark suites in `benchmarks/` and validate on your target hardware and network before making production commitments.
## Benchmarking
- See `benchmarks/README.md` for available benchmarks and how to run them.
- Run benchmarks on the same class of hardware you plan to deploy.
- Capture results with `-benchmem` and keep baselines in version control if needed.
## Practical Tuning Levers
- `server.tcp.plain.max_connections`: protect the broker from excess concurrent connections
- `session.max_sessions`: cap active MQTT sessions
- `broker.max_message_size`: limit payload size
- `session.max_offline_queue_size` and `session.max_inflight_messages`: control per-client memory usage
- `queues.*.limits`: bound queue depth, message size, and TTL
## OS and Runtime Considerations
- Ensure file descriptor limits are high enough for your target connection count.
- Tune TCP keepalive and timeouts based on your workload (IoT vs low-latency systems).
- Measure with production-like TLS settings if TLS is enabled in production.
## Cluster Scaling
- Clustering spreads sessions and subscriptions across nodes but requires etcd coordination.
- Use multiple nodes to improve availability and distribute connections.
- Queue replication is optional and configured via `cluster.raft`.
For configuration details, see `docs/configuration.md`.
+119
View File
@@ -0,0 +1,119 @@
---
title: Webhook System
description: Comprehensive webhook system for asynchronous event notifications with circuit breaker, retry logic, and flexible filtering
---
# Webhook System
**Last Updated:** 2026-02-05
FluxMQ can emit broker events to external HTTP endpoints using an asynchronous webhook notifier.
## Overview
- Asynchronous event queue with worker pool
- Retry with exponential backoff
- Circuit breaker per endpoint
- Filtering by event type and topic pattern
- HTTP sender only (gRPC sender not implemented)
## Architecture
```
Broker Events
Webhook Notifier (queue)
│ drop_policy: oldest/newest
Worker Pool
│ retry + circuit breaker
HTTP Sender
External Endpoints
```
## Event Types
Events are defined in `broker/events/events.go`.
- `client.connected`: `client_id`, `protocol`, `clean_start`, `keep_alive`, `remote_addr`
- `client.disconnected`: `client_id`, `reason`, `remote_addr`
- `client.session_takeover`: `client_id`, `from_node`, `to_node`
- `message.published`: `client_id`, `topic`, `qos`, `retained`, `payload_size`, `payload`
- `message.delivered`: `client_id`, `topic`, `qos`, `payload_size`
- `message.retained`: `topic`, `payload_size`, `cleared`
- `subscription.created`: `client_id`, `topic_filter`, `qos`, `subscription_id`
- `subscription.removed`: `client_id`, `topic_filter`
- `auth.success`: `client_id`, `remote_addr`
- `auth.failure`: `client_id`, `reason`, `remote_addr`
- `authz.publish_denied`: `client_id`, `topic`, `reason`
- `authz.subscribe_denied`: `client_id`, `topic_filter`, `reason`
### Payload Notes
The `message.published` payload field is defined in the event schema (base64-encoded when populated), but payload inclusion is not currently wired in the broker. `webhook.include_payload` is accepted in config, yet payloads are sent as empty strings at the moment.
## Event Envelope
```json
{
"event_type": "message.published",
"event_id": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-02-05T12:00:00Z",
"broker_id": "broker-1",
"data": {
"client_id": "publisher-1",
"topic": "sensors/temperature",
"qos": 1,
"retained": false,
"payload_size": 256
}
}
```
## Filtering
Each endpoint can filter by:
- `events`: list of event types
- `topic_filters`: MQTT-style patterns (supports `+` and `#`)
## Retry and Circuit Breaker
- Retries use exponential backoff (`initial_interval * multiplier^attempt`), capped by `max_interval`
- Circuit breaker is per endpoint and trips after `failure_threshold` consecutive failures
## Configuration
```yaml
webhook:
enabled: true
queue_size: 10000
drop_policy: "oldest" # oldest or newest
workers: 5
include_payload: false
shutdown_timeout: "30s"
defaults:
timeout: "5s"
retry:
max_attempts: 3
initial_interval: "1s"
max_interval: "30s"
multiplier: 2.0
circuit_breaker:
failure_threshold: 5
reset_timeout: "60s"
endpoints:
- name: "analytics"
type: "http"
url: "https://example.com/webhook"
events: ["message.published"]
topic_filters: ["sensors/#"]
headers:
Authorization: "Bearer token"
```
+1
View File
@@ -0,0 +1 @@
/// <reference types="astro/client" />
+230
View File
@@ -0,0 +1,230 @@
---
import '../styles/global.css';
import '../styles/fumadocs-search.css';
import HomeSearchBridge from '@/components/HomeSearchBridge';
interface Props {
title?: string;
description?: string;
}
const {
title = 'FluxMQ - High-Performance Multi-Protocol Message Broker',
description =
'FluxMQ is a high-performance, multi-protocol message broker written in Go. Supports MQTT, WebSocket, HTTP and CoAP with durable queues and clustering.',
} = Astro.props;
---
<!doctype html>
<html lang="en" class="scroll-smooth">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title}</title>
<meta name="description" content={description} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:type" content="website" />
<meta property="og:image" content="/og-image.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content="/og-image.png" />
<link rel="icon" href="/logo.png" />
<link rel="canonical" href="https://absmach.eu/fluxmq/" />
<script is:inline>
(() => {
const THEME_KEY = 'theme';
const stored = localStorage.getItem(THEME_KEY);
const mode = stored === 'light' || stored === 'dark' || stored === 'system' ? stored : 'system';
const preferred = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
const resolved = mode === 'system' ? preferred : mode;
document.documentElement.dataset.theme = resolved;
document.documentElement.classList.toggle('dark', resolved === 'dark');
})();
</script>
</head>
<body class="flex min-h-screen flex-col bg-theme text-[var(--flux-text)]">
<HomeSearchBridge client:only="react" />
<header class="site-header z-50">
<nav class="mx-auto flex min-h-[64px] w-[min(100%-2.5rem,1360px)] items-center gap-4" aria-label="Main navigation">
<a class="animate-fade-in text-2xl font-bold md:text-[2.45rem]" style="line-height:1.1" href="/" aria-label="FluxMQ home">
<span class="text-[var(--flux-blue)]">Flux</span><span class="text-[var(--flux-orange)]">MQ</span>
</a>
<button
class="inline-flex rounded-md border border-[var(--flux-pill-border)] bg-[var(--flux-pill-bg)] px-3 py-2 text-sm font-semibold text-[var(--flux-text)] md:hidden"
type="button"
aria-expanded="false"
aria-controls="site-menu"
data-menu-toggle
>
Menu
</button>
<ul
id="site-menu"
class="absolute left-0 right-0 top-[63px] hidden flex-col items-start gap-4 border-b border-[var(--flux-header-border)] bg-[var(--flux-header-bg)] px-5 py-4 text-[0.95rem] font-normal md:static md:top-0 md:flex md:min-w-0 md:flex-1 md:flex-row md:items-center md:gap-7 md:border-0 md:bg-transparent md:p-0 md:pl-8"
data-menu
>
<li><a class="nav-link" href="/#features">Features</a></li>
<li><a class="nav-link" href="/#performance">Performance</a></li>
<li><a class="nav-link" href="/#use-cases">Use Cases</a></li>
<li><a class="nav-link" href="/#architecture">Architecture</a></li>
<li><a class="nav-link" href="/#quick-start">Quick Start</a></li>
<li><a class="nav-link" href="/docs/">Documentation</a></li>
<li class="md:hidden"><a class="nav-link" href="https://github.com/absmach/fluxmq" target="_blank" rel="noopener noreferrer">GitHub</a></li>
</ul>
<div class="ml-auto hidden items-center gap-2 md:flex">
<button type="button" class="nav-search-pill" aria-label="Search docs" data-search-trigger>
<span class="inline-flex items-center gap-2">
<svg class="size-4" viewBox="0 0 20 20" fill="none" aria-hidden="true">
<circle cx="9" cy="9" r="5.5" stroke="currentColor" stroke-width="1.6"></circle>
<path d="M13 13L17 17" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"></path>
</svg>
<span>Search</span>
</span>
<span class="inline-flex items-center gap-1">
<kbd class="kbd-chip" data-shortcut-mod>Ctrl</kbd>
<kbd class="kbd-chip">K</kbd>
</span>
</button>
<button type="button" class="theme-toggle-pill" aria-label="Toggle theme" data-theme-toggle>
<span class="theme-dot" data-theme-sun aria-hidden="true">
<svg class="size-3.5" viewBox="0 0 20 20" fill="none">
<circle cx="10" cy="10" r="3.2" stroke="currentColor" stroke-width="1.6"></circle>
<path d="M10 2v2.2M10 15.8V18M2 10h2.2M15.8 10H18M4.6 4.6l1.5 1.5M13.9 13.9l1.5 1.5M15.4 4.6l-1.5 1.5M6.1 13.9l-1.5 1.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"></path>
</svg>
</span>
<span class="theme-dot" data-theme-moon aria-hidden="true">
<svg class="size-3.5" viewBox="0 0 20 20" fill="none">
<path d="M13.8 13.9A6.2 6.2 0 0 1 6 6.2a6.7 6.7 0 1 0 7.8 7.7Z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"></path>
</svg>
</span>
</button>
<a
class="nav-icon-link"
href="https://github.com/absmach/fluxmq"
target="_blank"
rel="noopener noreferrer"
aria-label="FluxMQ GitHub repository"
>
<svg class="size-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 .5C5.65.5.5 5.66.5 12.03c0 5.1 3.3 9.43 7.88 10.96.58.11.79-.25.79-.57 0-.28-.01-1.04-.02-2.04-3.2.7-3.88-1.55-3.88-1.55-.53-1.35-1.28-1.7-1.28-1.7-1.05-.72.08-.71.08-.71 1.15.08 1.76 1.2 1.76 1.2 1.03 1.76 2.7 1.25 3.37.96.1-.75.4-1.25.72-1.54-2.56-.29-5.26-1.29-5.26-5.74 0-1.27.46-2.3 1.2-3.11-.12-.29-.52-1.46.12-3.04 0 0 .98-.32 3.2 1.19a11.1 11.1 0 0 1 5.82 0c2.22-1.51 3.2-1.19 3.2-1.19.64 1.58.24 2.75.12 3.04.75.81 1.2 1.84 1.2 3.11 0 4.46-2.7 5.44-5.28 5.73.42.36.78 1.06.78 2.14 0 1.54-.01 2.78-.01 3.16 0 .31.21.68.8.56A11.54 11.54 0 0 0 23.5 12.03C23.5 5.66 18.35.5 12 .5Z" />
</svg>
</a>
</div>
<div class="ml-auto flex items-center gap-2 md:hidden">
<button type="button" class="theme-toggle-pill" aria-label="Toggle theme" data-theme-toggle>
<span class="theme-dot" data-theme-sun aria-hidden="true">
<svg class="size-3.5" viewBox="0 0 20 20" fill="none">
<circle cx="10" cy="10" r="3.2" stroke="currentColor" stroke-width="1.6"></circle>
<path d="M10 2v2.2M10 15.8V18M2 10h2.2M15.8 10H18M4.6 4.6l1.5 1.5M13.9 13.9l1.5 1.5M15.4 4.6l-1.5 1.5M6.1 13.9l-1.5 1.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"></path>
</svg>
</span>
<span class="theme-dot" data-theme-moon aria-hidden="true">
<svg class="size-3.5" viewBox="0 0 20 20" fill="none">
<path d="M13.8 13.9A6.2 6.2 0 0 1 6 6.2a6.7 6.7 0 1 0 7.8 7.7Z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"></path>
</svg>
</span>
</button>
<a
class="nav-icon-link"
href="https://github.com/absmach/fluxmq"
target="_blank"
rel="noopener noreferrer"
aria-label="FluxMQ GitHub repository"
>
<svg class="size-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 .5C5.65.5.5 5.66.5 12.03c0 5.1 3.3 9.43 7.88 10.96.58.11.79-.25.79-.57 0-.28-.01-1.04-.02-2.04-3.2.7-3.88-1.55-3.88-1.55-.53-1.35-1.28-1.7-1.28-1.7-1.05-.72.08-.71.08-.71 1.15.08 1.76 1.2 1.76 1.2 1.03 1.76 2.7 1.25 3.37.96.1-.75.4-1.25.72-1.54-2.56-.29-5.26-1.29-5.26-5.74 0-1.27.46-2.3 1.2-3.11-.12-.29-.52-1.46.12-3.04 0 0 .98-.32 3.2 1.19a11.1 11.1 0 0 1 5.82 0c2.22-1.51 3.2-1.19 3.2-1.19.64 1.58.24 2.75.12 3.04.75.81 1.2 1.84 1.2 3.11 0 4.46-2.7 5.44-5.28 5.73.42.36.78 1.06.78 2.14 0 1.54-.01 2.78-.01 3.16 0 .31.21.68.8.56A11.54 11.54 0 0 0 23.5 12.03C23.5 5.66 18.35.5 12 .5Z" />
</svg>
</a>
</div>
</nav>
</header>
<main>
<slot />
</main>
<script is:inline>
const THEME_KEY = 'theme';
const setTheme = (mode) => {
const preferred = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
const resolved = mode === 'system' ? preferred : mode;
document.documentElement.dataset.theme = resolved;
document.documentElement.classList.toggle('dark', resolved === 'dark');
localStorage.setItem(THEME_KEY, mode);
updateThemeIcons(resolved);
};
const updateThemeIcons = (resolved) => {
document.querySelectorAll('[data-theme-sun]').forEach((sun) => {
sun.classList.toggle('theme-dot-active', resolved === 'light');
});
document.querySelectorAll('[data-theme-moon]').forEach((moon) => {
moon.classList.toggle('theme-dot-active', resolved === 'dark');
});
};
document.querySelectorAll('[data-theme-toggle]').forEach((toggleTheme) => {
const current = document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light';
updateThemeIcons(current);
toggleTheme.addEventListener('click', () => {
const active = document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light';
const next = active === 'dark' ? 'light' : 'dark';
setTheme(next);
});
});
const openSearch = () => {
if (typeof window.__fluxmqOpenSearch === 'function') {
window.__fluxmqOpenSearch();
return;
}
window.dispatchEvent(new CustomEvent('fluxmq:open-search'));
};
const searchTrigger = document.querySelector('[data-search-trigger]');
if (searchTrigger) {
searchTrigger.addEventListener('click', openSearch);
}
const shortcutMod = document.querySelector('[data-shortcut-mod]');
if (shortcutMod && /(Mac|iPhone|iPad|iPod)/i.test(navigator.userAgent)) {
shortcutMod.textContent = '⌘';
}
window.addEventListener('keydown', (event) => {
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'k') {
event.preventDefault();
openSearch();
}
});
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', () => {
const stored = localStorage.getItem(THEME_KEY);
if (!stored || stored === 'system') setTheme('system');
});
const toggle = document.querySelector('[data-menu-toggle]');
const menu = document.querySelector('[data-menu]');
if (toggle && menu) {
toggle.addEventListener('click', () => {
const expanded = toggle.getAttribute('aria-expanded') === 'true';
toggle.setAttribute('aria-expanded', String(!expanded));
menu.classList.toggle('hidden', expanded);
menu.classList.toggle('flex', !expanded);
});
}
</script>
</body>
</html>
+54
View File
@@ -0,0 +1,54 @@
import type { Source } from 'fumadocs-core/source';
import { loader } from 'fumadocs-core/source';
import { type CollectionEntry, getCollection } from 'astro:content';
import * as path from 'node:path';
import type { StructuredData } from 'fumadocs-core/mdx-plugins';
export const source = loader({
source: await createSource(),
baseUrl: '/docs',
});
const docs = import.meta.glob('/src/content/docs/docs/**/*.{md,mdx}');
export async function getFullExport(entry: CollectionEntry<'docs'>) {
return (await docs['/' + entry.filePath!]()) as {
structuredData: StructuredData;
};
}
async function createSource() {
const out: Source<{
metaData: CollectionEntry<'meta'>['data'];
pageData: CollectionEntry<'docs'>['data'] & {
_raw: CollectionEntry<'docs'>;
};
}> = {
files: [],
};
for (const page of await getCollection('docs')) {
const virtualPath = path.relative('src/content/docs/docs', page.filePath!);
out.files.push({
type: 'page',
path: virtualPath,
data: {
...page.data,
_raw: page,
},
});
}
for (const meta of await getCollection('meta')) {
const virtualPath = path.relative('src/content/docs/docs', meta.filePath!);
out.files.push({
type: 'meta',
path: virtualPath,
data: meta.data,
});
}
return out;
}
+26
View File
@@ -0,0 +1,26 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
---
<BaseLayout title="Page Not Found | FluxMQ" description="The page you requested was not found.">
<section class="border-b-2 border-[var(--flux-border)] py-24">
<div class="mx-auto w-[min(100%-2.5rem,900px)]">
<h1 class="text-4xl font-extrabold md:text-6xl">404</h1>
<p class="mt-4 text-lg text-[var(--flux-muted)]">The page you requested could not be found.</p>
<div class="mt-8 flex flex-wrap gap-3">
<a
class="inline-block border-2 border-[var(--flux-border)] px-4 py-3 text-sm font-bold uppercase tracking-[0.02em] transition hover:bg-[var(--flux-blue)] hover:text-white"
href="/"
>
Go to Home
</a>
<a
class="inline-block border-2 border-[var(--flux-border)] px-4 py-3 text-sm font-bold uppercase tracking-[0.02em] transition hover:bg-[var(--flux-orange)] hover:text-black"
href="/docs/"
>
Open Docs
</a>
</div>
</div>
</section>
</BaseLayout>
+26
View File
@@ -0,0 +1,26 @@
import type { APIRoute } from 'astro';
import { createFromSource } from 'fumadocs-core/search/server';
import { getFullExport, source } from '@/lib/source';
import { getBreadcrumbItems } from 'fumadocs-core/breadcrumb';
const server = createFromSource(source, {
async buildIndex(page) {
const exported = await getFullExport(page.data._raw);
const structuredData = exported.structuredData ?? { headings: [], contents: [] };
return {
id: page.data._raw.id,
title: page.data.title,
description: page.data.description,
structuredData,
url: page.url,
breadcrumbs: getBreadcrumbItems(page.url, source.getPageTree()).map((item) =>
String(item.name),
),
};
},
});
export const GET: APIRoute = () => {
return server.staticGET();
};
+48
View File
@@ -0,0 +1,48 @@
---
import { render, type CollectionEntry } from 'astro:content';
import { Docs } from '@/components/docs';
import defaultMdxComponents from 'fumadocs-ui/mdx';
import '@/styles/docs.css';
import { source } from '@/lib/source';
import Layout from '@/components/layout.astro';
import type { TOCItemType } from 'fumadocs-core/toc';
interface Props {
page: CollectionEntry<'docs'>;
}
export async function getStaticPaths() {
return source.getPages().map((page) => ({
params: { slug: page.slugs.length > 0 ? page.slugs.join('/') : undefined },
props: { page: page.data._raw },
}));
}
const { page } = Astro.props;
const { Content, headings } = await render(page);
const toc: TOCItemType[] = headings.map((heading) => ({
depth: heading.depth,
title: heading.text,
url: `#${heading.slug}`,
}));
---
<Layout>
<title>{page.data.title}</title>
<meta name="title" content={page.data.title} />
<meta name="description" content={page.data.description} />
<Docs
tree={source.getPageTree()}
pathname={Astro.url.pathname}
params={Astro.params as Record<string, string | string[]>}
page={{ toc }}
client:load
>
<h1 class="text-3xl font-semibold">{page.data.title}</h1>
<p class="mb-8 text-lg text-fd-muted-foreground">{page.data.description}</p>
<div class="prose flex-1">
<Content components={{ ...defaultMdxComponents }} />
</div>
</Docs>
</Layout>
+22
View File
@@ -0,0 +1,22 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import ArchitectureSection from '../components/home/ArchitectureSection.astro';
import FeaturesSection from '../components/home/FeaturesSection.astro';
import FooterSection from '../components/home/FooterSection.astro';
import HeroSection from '../components/home/HeroSection.astro';
import NewsletterSection from '../components/home/NewsletterSection.astro';
import PerformanceSection from '../components/home/PerformanceSection.astro';
import QuickStartSection from '../components/home/QuickStartSection.astro';
import UseCasesSection from '../components/home/UseCasesSection.astro';
---
<BaseLayout>
<HeroSection />
<FeaturesSection />
<PerformanceSection />
<UseCasesSection />
<ArchitectureSection />
<QuickStartSection />
<NewsletterSection />
<FooterSection />
</BaseLayout>
+268
View File
@@ -0,0 +1,268 @@
@import 'tailwindcss';
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css';
:root {
--flux-blue: hsl(213.64deg 58.41% 44.31%);
--flux-orange: hsl(35.07deg 94.52% 57.06%);
--flux-bg: hsl(0 0% 100%);
--flux-fg: hsl(0 0% 10%);
--flux-border: hsl(0 0% 20%);
--flux-grid: hsl(0 0% 90%);
--flux-text: hsl(0 0% 10%);
--flux-text-muted: hsl(0 0% 40%);
--flux-bg-alt: hsl(0 0% 97%);
--color-fd-background: var(--flux-bg);
--color-fd-foreground: var(--flux-text);
--color-fd-muted: color-mix(in srgb, var(--flux-bg) 92%, var(--flux-fg));
--color-fd-muted-foreground: var(--flux-text-muted);
--color-fd-popover: color-mix(in srgb, var(--flux-bg) 96%, var(--flux-fg));
--color-fd-popover-foreground: var(--flux-text);
--color-fd-card: color-mix(in srgb, var(--flux-bg) 98%, var(--flux-fg));
--color-fd-card-foreground: var(--flux-text);
--color-fd-border: color-mix(in srgb, var(--flux-border) 35%, transparent);
--color-fd-primary: var(--flux-blue);
--color-fd-primary-foreground: var(--flux-bg);
--color-fd-secondary: color-mix(in srgb, var(--flux-bg) 90%, var(--flux-fg));
--color-fd-secondary-foreground: var(--flux-text);
--color-fd-accent: color-mix(in srgb, var(--flux-blue) 14%, transparent);
--color-fd-accent-foreground: var(--flux-text);
--color-fd-ring: color-mix(in srgb, var(--flux-orange) 60%, transparent);
}
:root[class~='dark'],
:root[data-theme='dark'] {
--flux-blue: hsl(213.64deg 70% 60%);
--flux-orange: hsl(35.07deg 94.52% 57.06%);
--flux-bg: hsl(0 0% 10%);
--flux-fg: hsl(0 0% 95%);
--flux-border: hsl(0 0% 30%);
--flux-grid: hsl(0 0% 20%);
--flux-text: hsl(0 0% 95%);
--flux-text-muted: hsl(0 0% 60%);
--flux-bg-alt: hsl(0 0% 8%);
--color-fd-background: var(--flux-bg);
--color-fd-foreground: var(--flux-text);
--color-fd-muted: color-mix(in srgb, var(--flux-bg) 82%, var(--flux-fg));
--color-fd-muted-foreground: var(--flux-text-muted);
--color-fd-popover: color-mix(in srgb, var(--flux-bg) 90%, black);
--color-fd-popover-foreground: var(--flux-text);
--color-fd-card: color-mix(in srgb, var(--flux-bg) 88%, black);
--color-fd-card-foreground: var(--flux-text);
--color-fd-border: color-mix(in srgb, var(--flux-border) 62%, transparent);
--color-fd-primary: var(--flux-orange);
--color-fd-primary-foreground: hsl(0 0% 10%);
--color-fd-secondary: color-mix(in srgb, var(--flux-bg) 78%, var(--flux-fg));
--color-fd-secondary-foreground: var(--flux-text);
--color-fd-accent: color-mix(in srgb, var(--flux-blue) 23%, transparent);
--color-fd-accent-foreground: var(--flux-text);
--color-fd-ring: color-mix(in srgb, var(--flux-orange) 70%, transparent);
}
html {
scroll-behavior: smooth;
}
body {
font-family: Verdana, Geneva, sans-serif;
}
p {
font-size: 1.0625rem;
line-height: 1.7;
}
.mono {
font-family: 'JetBrains Mono', 'Courier New', monospace;
}
.grid-pattern {
background-image:
linear-gradient(to right, var(--flux-grid) 1px, transparent 1px),
linear-gradient(to bottom, var(--flux-grid) 1px, transparent 1px);
background-size: 20px 20px;
}
.brutalist-border {
border: 2px solid var(--flux-border);
}
.brutalist-card {
border: 2px solid var(--flux-border);
background: var(--flux-bg);
color: var(--flux-text);
}
.brutalist-card:hover {
border-color: var(--flux-orange);
}
.terminal {
background: hsl(0 0% 10%);
color: hsl(120 100% 80%);
font-family: 'JetBrains Mono', 'Courier New', monospace;
border: 2px solid var(--flux-border);
}
:root[class~='dark'] .terminal,
:root[data-theme='dark'] .terminal {
background: hsl(0 0% 5%);
border-color: var(--flux-border);
}
.accent-line {
position: relative;
}
.accent-line::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 4px;
height: 100%;
background: var(--flux-orange);
}
.metrics-table {
border-collapse: separate;
border-spacing: 0;
}
.metrics-table th,
.metrics-table td {
border: 1px solid var(--flux-border);
padding: 0.75rem 1rem;
text-align: left;
}
.metrics-table th {
background: var(--flux-fg);
color: var(--flux-bg);
font-weight: bold;
}
.metrics-table tbody {
background: var(--flux-bg);
color: var(--flux-text);
}
.bg-theme {
background: var(--flux-bg);
color: var(--flux-text);
}
.bg-theme-alt {
background: var(--flux-bg-alt);
color: var(--flux-text);
}
.bg-theme-inverse {
background: var(--flux-fg);
color: var(--flux-bg);
}
.text-theme {
color: var(--flux-text);
}
.text-theme-muted {
color: var(--flux-text-muted);
}
.border-theme {
border-color: var(--flux-border);
}
*:focus-visible {
outline: 2px solid var(--flux-orange);
outline-offset: 2px;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fadeIn 0.8s ease-out;
}
.animate-slide-up {
animation: slideUp 0.8s ease-out;
animation-fill-mode: backwards;
}
.animate-slide-up:nth-child(2) {
animation-delay: 0.2s;
}
.animate-slide-up:nth-child(3) {
animation-delay: 0.4s;
}
@media (prefers-reduced-motion: reduce) {
.animate-fade-in,
.animate-slide-up {
animation: none;
}
}
#nd-sidebar [data-active='true'] {
color: var(--flux-orange) !important;
background-color: rgb(249 163 42 / 10%) !important;
border-left: 3px solid var(--flux-orange) !important;
}
#nd-sidebar a:hover {
color: var(--flux-blue) !important;
}
#nd-sidebar [data-search-full] {
border-color: color-mix(in srgb, var(--flux-border) 35%, transparent);
background: color-mix(in srgb, var(--flux-bg) 88%, var(--flux-fg));
}
#nd-sidebar [data-search-full] kbd {
border-color: color-mix(in srgb, var(--flux-border) 32%, transparent);
background: var(--flux-bg);
}
#nd-sidebar > div:last-child {
border-top-color: color-mix(in srgb, var(--flux-border) 30%, transparent);
}
#nd-toc {
border-left: 1px solid color-mix(in srgb, var(--flux-border) 24%, transparent);
padding-left: 1rem;
}
@media (min-width: 48rem) {
#nd-docs-layout {
--fd-sidebar-width: 312px;
--fd-toc-width: 272px;
}
#nd-subnav {
display: none !important;
}
}
+3
View File
@@ -0,0 +1,3 @@
@import 'tailwindcss';
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css';
+341
View File
@@ -0,0 +1,341 @@
@import 'tailwindcss';
:root {
--flux-blue: hsl(213.64deg 58.41% 44.31%);
--flux-orange: hsl(35.07deg 94.52% 57.06%);
--flux-bg: hsl(0 0% 100%);
--flux-fg: hsl(0 0% 10%);
--flux-border: hsl(0 0% 20%);
--flux-grid: hsl(0 0% 90%);
--flux-text: hsl(0 0% 10%);
--flux-text-muted: hsl(0 0% 40%);
--flux-bg-alt: hsl(0 0% 97%);
--flux-muted: var(--flux-text-muted);
--flux-header-bg: hsl(0 0% 97%);
--flux-header-border: hsl(0 0% 85%);
--flux-pill-bg: hsl(0 0% 96%);
--flux-pill-border: hsl(0 0% 82%);
--flux-pill-text: hsl(0 0% 45%);
--flux-icon: hsl(0 0% 15%);
--flux-focus-ring: color-mix(in srgb, var(--flux-orange) 65%, transparent);
}
:root[class~='dark'],
:root[data-theme='dark'] {
--flux-blue: hsl(213.64deg 70% 60%);
--flux-orange: hsl(35.07deg 94.52% 57.06%);
--flux-bg: hsl(0 0% 10%);
--flux-fg: hsl(0 0% 95%);
--flux-border: hsl(0 0% 30%);
--flux-grid: hsl(0 0% 20%);
--flux-text: hsl(0 0% 95%);
--flux-text-muted: hsl(0 0% 60%);
--flux-bg-alt: hsl(0 0% 8%);
--flux-muted: var(--flux-text-muted);
--flux-header-bg: hsl(0 0% 8%);
--flux-header-border: hsl(0 0% 18%);
--flux-pill-bg: hsl(0 0% 10%);
--flux-pill-border: hsl(0 0% 20%);
--flux-pill-text: hsl(0 0% 70%);
--flux-icon: hsl(0 0% 92%);
--flux-focus-ring: color-mix(in srgb, var(--flux-orange) 70%, transparent);
}
body {
font-family: Verdana, Geneva, sans-serif;
color: var(--flux-text);
background: var(--flux-bg);
}
p {
font-size: 1.0625rem;
line-height: 1.7;
}
.mono {
font-family: 'JetBrains Mono', 'Courier New', monospace;
}
.grid-pattern {
background-image:
linear-gradient(to right, var(--flux-grid) 1px, transparent 1px),
linear-gradient(to bottom, var(--flux-grid) 1px, transparent 1px);
background-size: 20px 20px;
}
.brutalist-border {
border: 2px solid var(--flux-border);
}
.brutalist-card {
border: 2px solid var(--flux-border);
background: var(--flux-bg);
color: var(--flux-text);
}
.brutalist-card:hover {
border-color: var(--flux-orange);
}
.terminal {
background: hsl(0 0% 10%);
color: hsl(120 100% 80%);
font-family: 'JetBrains Mono', 'Courier New', monospace;
border: 2px solid var(--flux-border);
}
:root[class~='dark'] .terminal,
:root[data-theme='dark'] .terminal {
background: hsl(0 0% 5%);
}
.accent-line {
position: relative;
}
.accent-line::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 4px;
height: 100%;
background: var(--flux-orange);
}
.metrics-table {
border-collapse: separate;
border-spacing: 0;
}
.metrics-table th,
.metrics-table td {
border: 1px solid var(--flux-border);
padding: 0.75rem 1rem;
text-align: left;
}
.metrics-table th {
background: var(--flux-fg);
color: var(--flux-bg);
font-weight: 700;
}
.metrics-table tbody {
background: var(--flux-bg);
color: var(--flux-text);
}
.bg-theme {
background: var(--flux-bg);
color: var(--flux-text);
}
.bg-theme-alt {
background: var(--flux-bg-alt);
color: var(--flux-text);
}
.text-theme-muted {
color: var(--flux-text-muted);
}
.border-theme {
border-color: var(--flux-border);
}
@layer components {
.site-header {
border-top: 6px solid hsl(0 0% 22%);
border-bottom: 1px solid var(--flux-header-border);
background: var(--flux-header-bg);
}
.nav-link {
@apply inline-flex text-[1.03rem] font-medium transition-colors;
color: color-mix(in srgb, var(--flux-text) 65%, transparent);
}
.nav-link:hover {
color: var(--flux-text);
}
.nav-search-pill {
@apply inline-flex min-w-[252px] items-center justify-between rounded-full border px-4 py-2 text-[1.05rem] leading-none transition;
border-color: var(--flux-pill-border);
background: var(--flux-pill-bg);
color: var(--flux-pill-text);
}
.nav-search-pill:hover {
border-color: color-mix(in srgb, var(--flux-blue) 35%, var(--flux-pill-border));
color: color-mix(in srgb, var(--flux-text) 75%, transparent);
}
.kbd-chip {
@apply inline-flex items-center justify-center rounded-md border px-1.5 py-[0.14rem] text-[0.72rem] font-semibold;
border-color: var(--flux-pill-border);
background: var(--flux-bg);
color: var(--flux-pill-text);
}
.theme-toggle-pill {
@apply inline-flex items-center gap-0.5 rounded-full border p-[0.2rem];
border-color: var(--flux-pill-border);
background: var(--flux-pill-bg);
}
.theme-toggle-pill:hover {
border-color: color-mix(in srgb, var(--flux-orange) 45%, var(--flux-pill-border));
}
.theme-dot {
@apply inline-flex size-6 items-center justify-center rounded-full transition-colors;
color: var(--flux-pill-text);
}
.theme-dot-active {
background: var(--flux-bg);
color: var(--flux-icon);
box-shadow: 0 0 0 1px var(--flux-pill-border);
}
.nav-icon-link {
@apply inline-flex items-center justify-center rounded-full p-1 transition-colors;
color: var(--flux-icon);
}
.nav-icon-link:hover {
color: var(--flux-orange);
}
.feature-card {
@apply border-2 bg-[var(--flux-bg)] p-5;
border-color: var(--flux-border);
color: var(--flux-text);
}
.feature-card:hover {
border-color: var(--flux-orange);
}
.feature-card h3 {
@apply mb-3 text-xl font-bold;
}
.feature-card p {
color: var(--flux-text-muted);
}
.bullet-list {
list-style-type: square;
@apply space-y-1.5 pl-5;
color: var(--flux-text-muted);
}
.cluster-box {
@apply border-2 p-5;
border-color: var(--flux-border);
background: var(--flux-bg);
color: var(--flux-text);
}
.metric-head {
@apply border px-3 py-3 text-left text-sm font-bold uppercase md:text-base;
border-color: var(--flux-border);
background: var(--flux-fg);
color: var(--flux-bg);
}
.metric-cell {
@apply border px-3 py-3 text-sm md:text-base;
border-color: var(--flux-border);
}
.code-panel {
@apply relative border-2;
border-color: var(--flux-border);
background: hsl(0 0% 10%);
color: hsl(120 100% 80%);
font-family: 'JetBrains Mono', 'Courier New', monospace;
}
.code-panel pre {
@apply m-0 overflow-x-auto p-4;
}
.code-panel code {
@apply text-[0.95rem] whitespace-pre;
font-family: 'JetBrains Mono', 'Courier New', monospace;
}
}
.network-dash line {
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-dasharray: 8 10;
animation: network-dash 10s linear infinite;
}
.animate-fade-in {
animation: fadeIn 0.8s ease-out;
}
.animate-slide-up {
animation: slideUp 0.8s ease-out;
animation-fill-mode: backwards;
}
.animate-slide-up:nth-child(2) {
animation-delay: 0.2s;
}
.animate-slide-up:nth-child(3) {
animation-delay: 0.4s;
}
@media (prefers-reduced-motion: reduce) {
.network-dash line,
.animate-fade-in,
.animate-slide-up {
animation: none;
}
}
@keyframes network-dash {
from {
stroke-dashoffset: 0;
}
to {
stroke-dashoffset: -420;
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
:focus-visible {
outline: 2px solid var(--flux-focus-ring);
outline-offset: 2px;
}
+240
View File
@@ -0,0 +1,240 @@
:root,
:root[data-theme='light'] {
--flux-blue: hsl(213.64deg 58.41% 44.31%);
--flux-orange: hsl(35.07deg 94.52% 57.06%);
--flux-bg: hsl(0 0% 100%);
--flux-fg: hsl(0 0% 10%);
--flux-border: hsl(0 0% 20%);
--flux-text: hsl(0 0% 10%);
--flux-text-muted: hsl(0 0% 40%);
--flux-bg-alt: hsl(0 0% 97%);
--sl-font: Verdana, Geneva, sans-serif;
--sl-font-mono: 'JetBrains Mono', 'Courier New', monospace;
--sl-color-accent-low: color-mix(in srgb, var(--flux-blue) 18%, white);
--sl-color-accent: var(--flux-blue);
--sl-color-accent-high: color-mix(in srgb, var(--flux-blue) 70%, black);
--sl-color-text: var(--flux-text);
--sl-color-text-accent: var(--flux-blue);
--sl-color-text-invert: var(--flux-bg);
--sl-color-bg: var(--flux-bg);
--sl-color-bg-nav: var(--flux-bg);
--sl-color-bg-sidebar: var(--flux-bg);
--sl-color-bg-inline-code: color-mix(in srgb, var(--flux-fg) 8%, white);
--sl-color-bg-accent: var(--flux-blue);
--sl-color-hairline-light: color-mix(in srgb, var(--flux-border) 30%, white);
--sl-color-hairline: color-mix(in srgb, var(--flux-border) 45%, white);
--sl-color-hairline-shade: color-mix(in srgb, var(--flux-border) 60%, white);
--sl-sidebar-width: 21rem;
}
:root[data-theme='dark'] {
--flux-blue: hsl(213.64deg 70% 60%);
--flux-orange: hsl(35.07deg 94.52% 57.06%);
--flux-bg: hsl(0 0% 10%);
--flux-fg: hsl(0 0% 95%);
--flux-border: hsl(0 0% 30%);
--flux-text: hsl(0 0% 95%);
--flux-text-muted: hsl(0 0% 60%);
--flux-bg-alt: hsl(0 0% 8%);
--sl-font: Verdana, Geneva, sans-serif;
--sl-font-mono: 'JetBrains Mono', 'Courier New', monospace;
--sl-color-accent-low: color-mix(in srgb, var(--flux-blue) 28%, black);
--sl-color-accent: var(--flux-blue);
--sl-color-accent-high: color-mix(in srgb, var(--flux-blue) 70%, white);
--sl-color-text: var(--flux-text);
--sl-color-text-accent: var(--flux-blue);
--sl-color-text-invert: var(--flux-bg);
--sl-color-bg: var(--flux-bg);
--sl-color-bg-nav: var(--flux-bg);
--sl-color-bg-sidebar: var(--flux-bg-alt);
--sl-color-bg-inline-code: color-mix(in srgb, var(--flux-fg) 12%, black);
--sl-color-bg-accent: var(--flux-blue);
--sl-color-hairline-light: color-mix(in srgb, var(--flux-border) 60%, black);
--sl-color-hairline: color-mix(in srgb, var(--flux-border) 70%, black);
--sl-color-hairline-shade: color-mix(in srgb, var(--flux-border) 80%, black);
--sl-sidebar-width: 21rem;
}
.page > .header {
border-bottom: 2px solid var(--flux-border);
}
.sidebar-pane {
border-right: 2px solid color-mix(in srgb, var(--flux-border) 28%, transparent);
}
.sidebar-content a[aria-current='page'] {
color: var(--flux-orange) !important;
background-color: color-mix(in srgb, var(--flux-orange) 12%, transparent) !important;
border-left: 3px solid var(--flux-orange) !important;
}
.sidebar-content a:hover {
color: var(--flux-blue) !important;
}
.site-title {
font-weight: 800;
}
.site-title img {
display: none;
}
.site-title span {
position: relative;
font-size: 0;
letter-spacing: normal;
}
.site-title span::before {
content: 'Flux';
font-size: 1.55rem;
font-weight: 800;
color: var(--flux-blue);
}
.site-title span::after {
content: 'MQ';
font-size: 1.55rem;
font-weight: 800;
color: var(--flux-orange);
}
.sl-markdown-content p {
font-size: 1.0625rem;
line-height: 1.7;
}
.sl-markdown-content table th,
.sl-markdown-content table td {
border: 1px solid var(--flux-border);
}
.sl-markdown-content table th {
background: var(--flux-fg);
color: var(--flux-bg);
font-weight: 700;
}
@media (min-width: 50rem) {
:root,
:root[data-theme='light'],
:root[data-theme='dark'] {
--sl-nav-height: 7.2rem;
}
.page {
display: grid !important;
grid-template-columns: var(--sl-sidebar-width) minmax(0, 1fr);
grid-template-rows: var(--sl-nav-height) minmax(0, 1fr);
min-height: 100vh;
}
.page > .header {
grid-column: 1;
grid-row: 1;
width: var(--sl-sidebar-width) !important;
border-inline-end: 1px solid var(--sl-color-hairline);
background: var(--sl-color-bg-sidebar);
}
.page > .header > .header {
display: flex;
height: 100%;
flex-direction: column;
justify-content: center;
gap: 0.7rem;
padding-inline: 0.8rem;
background: transparent;
border: 0;
}
.page > .header .title-wrapper {
width: 100%;
}
.page > .header site-search {
width: 100%;
}
.page > .header site-search button[data-open-modal] {
width: 100%;
justify-content: space-between;
border: 1px solid var(--sl-color-hairline-light);
border-radius: 0.6rem;
background: color-mix(in srgb, var(--sl-color-bg) 75%, var(--sl-color-bg-sidebar));
}
.page > .header site-search button[data-open-modal] kbd {
display: inline-flex !important;
}
.page > .header .right-group {
display: none !important;
}
.page > .sidebar {
grid-column: 1;
grid-row: 2;
}
.page > .sidebar .sidebar-pane {
inset-block-start: var(--sl-nav-height);
width: var(--sl-sidebar-width);
}
.page > .sidebar .sidebar-content {
min-height: calc(100vh - var(--sl-nav-height));
}
.page > .sidebar .sidebar-content > .md\:sl-hidden {
display: block !important;
margin-top: auto;
padding-top: 0.75rem;
border-top: 1px solid var(--sl-color-hairline-light);
}
.page > .sidebar .mobile-preferences {
display: flex;
align-items: center;
border-top: 0;
padding-top: 0.3rem;
padding-bottom: 0.4rem;
}
.page > .sidebar .mobile-preferences .social-icons {
margin-inline-end: 0;
padding: 0;
}
.page > .sidebar .mobile-preferences starlight-theme-select label {
--sl-select-width: 2.1rem;
border: 1px solid var(--sl-color-hairline-light);
border-radius: 999px;
padding: 0.35rem;
min-width: auto;
}
.page > .sidebar .mobile-preferences starlight-theme-select .caret {
display: none;
}
.page > .sidebar .mobile-preferences starlight-theme-select select {
position: absolute;
inset: 0;
opacity: 0;
}
.page > .main-frame {
grid-column: 2;
grid-row: 1 / span 2;
padding-top: 0;
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
+4 -2
View File
@@ -23,11 +23,13 @@ function initOrama() {
}
export default function DefaultSearchDialog(props: SharedProps) {
const { locale } = useI18n(); // (optional) for i18n
const { locale, locales } = useI18n(); // (optional) for i18n
const searchLocale = locales && locales.length > 0 ? locale : undefined;
const { search, setSearch, query } = useDocsSearch({
type: 'static',
initOrama,
locale,
locale: searchLocale,
});
return (