feat: add GitHub Actions workflow for building and publishing core Docker image

- Introduced a new GitHub Actions workflow in `core-image.yml` to automate the build and publish process for the core Docker image.
- The workflow triggers on pushes to the main branch and supports manual dispatch.
- Configured steps for checking out the repository, setting up Docker Buildx, logging into the GitHub Container Registry, extracting Docker metadata, and building and pushing the Docker image.

Additionally, updated documentation to reflect changes in the deployment process, including a new section for multi-tenant SaaS deployment.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei
2025-11-23 20:22:23 +08:00
parent 848d9af672
commit 1c64070a5d
10 changed files with 501 additions and 159 deletions

53
.github/workflows/core-image.yml vendored Normal file
View File

@@ -0,0 +1,53 @@
name: Build & Publish core Docker image
on:
push:
branches:
- main
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: afilmory/afilmory-core
jobs:
build-and-push-core:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata (tags & labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest
type=sha,format=short
type=ref,event=branch
type=ref,event=tag
- name: Build and push Docker image (core)
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile.core
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -2,7 +2,7 @@
title: Deployment
description: Deploy your gallery to static hosting, SSR platforms, or Docker.
createdAt: 2025-07-20T22:35:03+08:00
lastModified: 2025-11-23T19:40:52+08:00
lastModified: 2025-11-23T20:22:24+08:00
order: 50
---
@@ -49,6 +49,12 @@ Bundle everything in a container for self-hosting or cloud deployment.
**Guide:**
- [Docker](/deployment/docker)
### Multi-tenant SaaS
Deploy the full backend + OAuth gateway with subdomains and custom domains.
- [Advanced SaaS Deployment](/deployment/saas)
## Deployment Workflow
### 1. Build Assets

View File

@@ -0,0 +1,160 @@
---
title: Advanced SaaS Deployment
description: Run the full multi-tenant stack (API + OAuth gateway) with subdomains and custom domains.
createdAt: 2025-11-23T19:50:00+08:00
lastModified: 2025-11-23T20:22:24+08:00
order: 60
---
# Advanced SaaS Deployment
This guide describes how to deploy the multi-tenant stack that serves many galleries from a single installation. It is based on the backend (`be/apps/core`) and the OAuth callback router (`be/apps/oauth-gateway`).
## Components
- **Core API + SPA host (`@afilmory/core`)** — serves the web app, injects `__MANIFEST__`/`__CONFIG__`, and resolves tenants by host.
- **OAuth Gateway (`@afilmory/oauth-gateway`)** — a tiny router that receives all provider callbacks at one domain and forwards them to the correct tenant subdomain.
- **PostgreSQL + Redis** — required by the core service.
- **Object storage** — wherever your photo originals live (S3/B2/GitHub/local, configured via builder/system settings).
## Quick start with Docker (all-in-one core image)
`Dockerfile.core` builds the core API, embeds web + dashboard, installs `exiftool`/`vips`, and runs migrations on start.
```bash
# Build image
docker build -f Dockerfile.core -t afilmory-core .
# Run (Postgres/Redis reachable)
docker run --rm \
-p 1841:1841 \
-e DATABASE_URL="postgres://user:pass@db:5432/afilmory" \
-e REDIS_URL="redis://redis:6379" \
-e HOSTNAME="core.local" \ # optional, affects printed URLs
-e NODE_ENV=production \
afilmory-core
```
First boot: container runs `db:migrate`, provisions the root tenant + superadmin, then prints credentials to stdout, e.g.
```
============================================================
Root dashboard access provisioned.
Dashboard URL: http://localhost:1841/platform/root-login
Alternate URL: http://core.local:1841/platform/root-login
Email: root@local.host
Username: root
Password: w3xF1cC5cE5Zf1l_yhGy6w
============================================================
```
- Email/username come from `DEFAULT_SUPERADMIN_EMAIL/USERNAME` (defaults `root@local.host` / `root`).
- Password is a random 16-byte `base64url` string generated once per fresh boot and **only shown in logs**.
## First-login flow
1. Open `https://root.<baseDomain>/platform/root-login` (or the printed dashboard URL locally).
2. Sign in with the printed root credentials.
3. In Superadmin, set system settings: `baseDomain`, `oauthGatewayUrl`, storage providers, registration rules, billing/storage plans, etc.
4. Create your first tenant (slug cannot be reserved: `root` and the placeholder slug are reserved).
5. Configure OAuth providers (Google/GitHub) with callback pointing to the OAuth Gateway.
## Domain model
- **Base domain** (default `afilmory.art`, configurable in System Settings): tenants are addressed as `<slug>.<baseDomain>`.
- **OAuth callback domain**: usually `auth.<baseDomain>` pointing to the OAuth Gateway.
- **Custom domains**: tenants can bind their own domain via DNS verification (CNAME to base domain *or* TXT containing a verification token).
## DNS layout
1) Point the **core** service to the base domain and its wildcard:
- `A/AAAA` or `CNAME` for `<baseDomain>` → core load balancer.
- `CNAME` `*.<baseDomain>` → core load balancer (enables tenant subdomains).
2) Point the **OAuth Gateway**:
- `CNAME auth.<baseDomain>` → gateway load balancer (or the same host if you co-locate).
3) For **custom domains**:
- Either add `CNAME <custom> -> <baseDomain>` **or**
- Add `TXT <custom> = <verificationToken>` (token issued when the tenant requests binding).
## Runtime configuration
### Core service (`be/apps/core`)
Key system settings (editable via admin UI/API):
- `baseDomain` — used to derive tenant slug from host; defaults to `afilmory.art`.
- `oauthGatewayUrl` — e.g., `https://auth.<baseDomain>`; appended to OAuth provider redirects.
Minimal env (SaaS core):
```
PORT=1841
DATABASE_URL=postgres://localhost:5432/afilmory
REDIS_URL=localhost:6379
CONFIG_ENCRYPTION_KEY=<openssl -hex 64>
```
`CONFIG_ENCRYPTION_KEY` is required to encrypt sensitive settings; generate with `openssl rand -hex 64`.
Set `NODE_ENV=production` in real deployments and add storage/OAuth creds as needed.
Deploy with `pnpm --filter @afilmory/core dev|start` (or containerize) and serve on HTTPS.
### OAuth Gateway (`be/apps/oauth-gateway`)
Env vars (mapped in `src/config.ts`):
```
AUTH_GATEWAY_HOST=0.0.0.0
AUTH_GATEWAY_PORT=8790
AUTH_GATEWAY_BASE_DOMAIN=your-base-domain.com
AUTH_GATEWAY_FORCE_HTTPS=true
AUTH_GATEWAY_CALLBACK_BASE_PATH=/api/auth/callback
AUTH_GATEWAY_ALLOW_CUSTOM_HOST=false
AUTH_GATEWAY_ROOT_SLUG=root
```
Run: `pnpm --filter @afilmory/oauth-gateway dev` (or containerize). Expose HTTPS at `auth.<baseDomain>`.
## Request routing flow
1. **Tenant resolution (core):**
- Try custom domain match (`TenantDomainService`).
- Else derive slug from `<slug>.<baseDomain>` (`extractTenantSlugFromHost`).
- Root/admin paths (`/api/super-admin`, `/api/settings`, `/api/storage/settings`, `/api/builder/settings`) fall back to `root` tenant.
- Missing slug falls back to a placeholder tenant so the app can render an onboarding state.
2. **OAuth callback (gateway):**
- Providers redirect to `https://auth.<baseDomain>/api/auth/callback/{provider}?tenantSlug=<slug>`.
- Gateway rewrites to `https://<slug>.<baseDomain>/api/auth/callback/{provider}` (preserving query), keeping tenant sessions isolated.
3. **Custom domain verification:**
- Tenant requests domain → receives a verification token.
- Verification passes if either CNAME points to `<baseDomain>` or TXT contains the token (checked via DNS `CNAME`/`TXT`).
## Tenant lifecycle
- **Create tenant:** super-admin API `PATCH/POST` paths under `/api/super-admin/tenants` (see dashboard or use API) with `slug` and optional billing/storage plan IDs.
- **Plans & quotas:** `BillingPlanService` and `BillingUsageService` enforce per-tenant limits; override pricing/products in System Settings.
- **Banning/suspension:** super-admin can ban or suspend; requests then raise `TENANT_BANNED`/`TENANT_SUSPENDED` errors.
## Deployment checklist
1. Provision Postgres + Redis; migrate DB via `pnpm --filter @afilmory/core db:migrate` (or your migration flow).
2. Deploy **core** behind HTTPS on `<baseDomain>` with wildcard support.
3. Deploy **OAuth Gateway** at `auth.<baseDomain>`; update System Settings `oauthGatewayUrl`.
4. Set System Settings `baseDomain` to your chosen domain; restart core if env-derived host/ports change.
5. Create a super-admin user and a first tenant (slug not reserved: `root`, placeholder slug are reserved).
6. Test: visit `https://<slug>.<baseDomain>` → login via OAuth → manifest injected → gallery loads.
7. Test custom domain: request binding, add CNAME/TXT, run verification; ensure HTTPS certs via your proxy/ingress.
## Licensing notice (commercial use)
The SaaS build includes billing (Creem, plan enforcement, usage tracking). The open-source license does **not** grant commercial rights to run this as a paid service. For commercial use or partnerships, contact the project owner for authorization.
## Notes
- `DEFAULT_BASE_DOMAIN` is `afilmory.art`; override in settings for your SaaS.
- Placeholder tenant slug is generated to let the UI show onboarding before a real slug exists.
- The gateway is stateless; you can scale it horizontally behind any load balancer.

View File

@@ -1,8 +1,8 @@
---
title: Docs site
title: Docs Site
description: Docs site introduction and contribution guide
createdAt: 2025-09-11T17:05:55+08:00
lastModified: 2025-11-23T19:40:52+08:00
lastModified: 2025-11-23T20:22:24+08:00
order: 90
---
@@ -41,6 +41,7 @@ pnpm build
```
This runs three steps:
1. `build:client` - Builds the client-side React application
2. `build:static` - Generates static HTML files via SSR
3. `output` - Processes and finalizes the build output
@@ -98,10 +99,7 @@ lastModified: 2025-01-20T10:00:00Z
Regular markdown content here.
<div className="bg-blue-100 p-4 rounded">
Custom JSX component
</div>
<div className="bg-blue-100 p-4 rounded">Custom JSX component</div>
```
### Frontmatter
@@ -110,10 +108,10 @@ Each documentation file can include frontmatter metadata:
```yaml
---
title: Page Title # Used for navigation and SEO
createdAt: 2025-01-20 # Creation date
lastModified: 2025-01-20 # Last modification date
description: Page description # Optional page description
title: Page Title # Used for navigation and SEO
createdAt: 2025-01-20 # Creation date
lastModified: 2025-01-20 # Last modification date
description: Page description # Optional page description
---
```
@@ -143,22 +141,23 @@ export function InfoBox({ children }: { children: React.ReactNode }) {
```mdx
<!-- In your MDX file -->
import { InfoBox } from '../src/components/InfoBox';
<InfoBox>
This is a custom info box component.
</InfoBox>
import { InfoBox } from '../src/components/InfoBox'
<InfoBox>This is a custom info box component.</InfoBox>
```
### Styling
The project uses:
- **Tailwind CSS** for utility-first styling
- **Apple UIKit colors** via `tailwindcss-uikit-colors`
- **Typography plugin** for prose styling
- **Custom scrollbar** styling
Use semantic color classes:
```css
/* Preferred */
.text-text-primary .bg-fill-secondary
@@ -191,6 +190,7 @@ Key configuration in `vite.config.ts`:
### Supported Languages
Code highlighting supports:
- JavaScript/TypeScript
- JSX/TSX
- MDX

View File

@@ -77,11 +77,11 @@ function App({ url }: { url?: string }) {
<main className="bg-background flex flex-1 items-center justify-center">
{/* 移动端顶部栏 */}
<div className="bg-background fixed top-0 right-0 left-0 z-30 h-16 border-b border-gray-200 lg:hidden">
<div className="fixed top-0 right-0 left-0 z-30 h-16 border-b border-zinc-200 bg-white/80 backdrop-blur-xl lg:hidden dark:border-zinc-800 dark:bg-zinc-950/80">
<div className="flex h-full items-center px-4">
<button
onClick={toggleSidebar}
className="text-text-primary hover:bg-background-secondary rounded-lg p-2 transition-colors"
className="rounded-lg p-2 text-zinc-700 transition-colors hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
type="button"
>
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -89,7 +89,7 @@ function App({ url }: { url?: string }) {
</svg>
</button>
<div className="flex-1 text-center">
<h1 className="text-text-primary text-lg font-semibold">Afilmory Docs</h1>
<h1 className="text-lg font-semibold text-zinc-900 dark:text-zinc-100">Afilmory Docs</h1>
</div>
<div className="w-10" /> {/* 平衡按钮 */}
</div>
@@ -149,11 +149,11 @@ function App({ url }: { url?: string }) {
{/* 主内容区域 */}
<main className="bg-background relative flex-1 overflow-y-auto" ref={mainContentRef}>
<div className="bg-background border-border sticky top-0 z-30 h-16 border-b backdrop-blur-3xl lg:hidden">
<div className="sticky top-0 z-30 h-16 border-b border-zinc-200 bg-white/80 backdrop-blur-xl lg:hidden dark:border-zinc-800 dark:bg-zinc-950/80">
<div className="flex h-full items-center px-4">
<button
onClick={toggleSidebar}
className="text-text-primary hover:bg-background-secondary rounded-lg p-2 transition-colors"
className="rounded-lg p-2 text-zinc-700 transition-colors hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
type="button"
>
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -162,7 +162,7 @@ function App({ url }: { url?: string }) {
</button>
<div className="flex-1 text-center">
<a href="/" className="select-none">
<h1 className="text-text-primary text-lg font-semibold">Afilmory Docs</h1>
<h1 className="text-lg font-semibold text-zinc-900 dark:text-zinc-100">Afilmory Docs</h1>
</a>
</div>
<div className="w-10" /> {/* 平衡按钮 */}

View File

@@ -17,7 +17,7 @@ export function MobileTableOfContents({ currentPath, handleScroll, scrollerEleme
{/* TOC 按钮 */}
<button
onClick={() => setIsOpen(!isOpen)}
className="hover:bg-material-opaque text-text-primary bg-material-opaque border-border fixed right-4 bottom-6 z-50 flex h-12 w-12 items-center justify-center rounded-full border backdrop-blur-xl transition-all duration-200 xl:hidden"
className="fixed right-4 bottom-6 z-50 flex h-12 w-12 items-center justify-center rounded-full border border-zinc-200 bg-white/90 text-zinc-700 shadow-lg backdrop-blur-xl transition-all duration-200 hover:bg-white hover:shadow-xl xl:hidden dark:border-zinc-800 dark:bg-zinc-900/90 dark:text-zinc-300 dark:hover:bg-zinc-900"
aria-label="Toggle TOC"
type="button"
>

View File

@@ -197,7 +197,7 @@ export function Sidebar({ currentPath, onNavigate }: SidebarProps) {
const navigationTree = buildNavigationTree(routes)
return (
<aside className="relative h-screen w-72 overflow-x-hidden overflow-y-auto border-r border-zinc-200 dark:border-zinc-800">
<aside className="lg:bg-background relative h-screen w-72 overflow-x-hidden overflow-y-auto border-r border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-950">
<HeaderLogoSection />
<nav className="space-y-0.5 px-2 py-3">

View File

@@ -171,7 +171,7 @@
"title": "Deployment",
"description": "Deploy your gallery to static hosting, SSR platforms, or Docker.",
"createdAt": "2025-07-20T22:35:03+08:00",
"lastModified": "2025-11-23T19:40:52+08:00",
"lastModified": "2025-11-23T20:22:24+08:00",
"order": "50"
}
},
@@ -220,13 +220,24 @@
}
},
{
"path": "/docs-site",
"title": "Docs site",
"path": "/deployment/saas",
"title": "Advanced SaaS Deployment",
"meta": {
"title": "Docs site",
"title": "Advanced SaaS Deployment",
"description": "Run the full multi-tenant stack (API + OAuth gateway) with subdomains and custom domains.",
"createdAt": "2025-11-23T19:50:00+08:00",
"lastModified": "2025-11-23T20:22:24+08:00",
"order": "60"
}
},
{
"path": "/docs-site",
"title": "Docs Site",
"meta": {
"title": "Docs Site",
"description": "Docs site introduction and contribution guide",
"createdAt": "2025-09-11T17:05:55+08:00",
"lastModified": "2025-11-23T19:40:52+08:00",
"lastModified": "2025-11-23T20:22:24+08:00",
"order": "90"
}
}

View File

@@ -13,8 +13,9 @@ import Route18 from '../contents/deployment/cloudflare-pages.mdx'
import Route16 from '../contents/deployment/docker.mdx'
import Route17 from '../contents/deployment/github-pages.mdx'
import Route15 from '../contents/deployment/index.mdx'
import Route20 from '../contents/deployment/saas.mdx'
import Route19 from '../contents/deployment/vercel.mdx'
import Route20 from '../contents/docs-site.mdx'
import Route21 from '../contents/docs-site.mdx'
import Route2 from '../contents/getting-started/quick-start.mdx'
import Route0 from '../contents/index.mdx'
import Route10 from '../contents/storage/providers/b2.mdx'
@@ -37,253 +38,265 @@ export const routes: RouteConfig[] = [
component: Route0,
title: 'Overview',
meta: {
title: 'Overview',
description: 'Deploy a photo gallery in minutes—no database required.',
createdAt: '2025-07-20T22:35:03+08:00',
lastModified: '2025-11-23T19:40:52+08:00',
order: '1',
},
"title": "Overview",
"description": "Deploy a photo gallery in minutes—no database required.",
"createdAt": "2025-07-20T22:35:03+08:00",
"lastModified": "2025-11-23T19:40:52+08:00",
"order": "1"
}
},
{
path: '/builder/cli',
component: Route1,
title: 'CLI & Run Modes',
meta: {
title: 'CLI & Run Modes',
description: 'Per-run flags, concurrency behavior, and cluster vs worker execution.',
createdAt: '2025-11-23T19:00:00+08:00',
lastModified: '2025-11-23T19:40:52+08:00',
order: '2',
},
"title": "CLI & Run Modes",
"description": "Per-run flags, concurrency behavior, and cluster vs worker execution.",
"createdAt": "2025-11-23T19:00:00+08:00",
"lastModified": "2025-11-23T19:40:52+08:00",
"order": "2"
}
},
{
path: '/getting-started/quick-start',
component: Route2,
title: 'Quick Start',
meta: {
title: 'Quick Start',
description: 'Get your gallery running in about 5 minutes.',
createdAt: '2025-11-14T22:20:00+08:00',
lastModified: '2025-11-23T19:40:52+08:00',
order: '2',
},
"title": "Quick Start",
"description": "Get your gallery running in about 5 minutes.",
"createdAt": "2025-11-14T22:20:00+08:00",
"lastModified": "2025-11-23T19:40:52+08:00",
"order": "2"
}
},
{
path: '/architecture',
component: Route3,
title: 'Architecture',
meta: {
title: 'Architecture',
description: 'Understand how Afilmory works—static vs. SSR deployment modes.',
createdAt: '2025-11-14T22:25:00+08:00',
lastModified: '2025-11-23T19:40:52+08:00',
order: '3',
},
"title": "Architecture",
"description": "Understand how Afilmory works—static vs. SSR deployment modes.",
"createdAt": "2025-11-14T22:25:00+08:00",
"lastModified": "2025-11-23T19:40:52+08:00",
"order": "3"
}
},
{
path: '/builder/config',
component: Route4,
title: 'Configuration',
meta: {
title: 'Configuration',
description: 'How to declare storage, system defaults, and plugins in builder.config.ts.',
createdAt: '2025-11-23T19:00:00+08:00',
lastModified: '2025-11-23T19:40:52+08:00',
order: '3',
},
"title": "Configuration",
"description": "How to declare storage, system defaults, and plugins in builder.config.ts.",
"createdAt": "2025-11-23T19:00:00+08:00",
"lastModified": "2025-11-23T19:40:52+08:00",
"order": "3"
}
},
{
path: '/builder/pipeline',
component: Route5,
title: 'Processing Flow',
meta: {
title: 'Processing Flow',
description: 'End-to-end pipeline, ordering, and incremental rules backed by the code.',
createdAt: '2025-11-23T19:00:00+08:00',
lastModified: '2025-11-23T19:40:52+08:00',
order: '4',
},
"title": "Processing Flow",
"description": "End-to-end pipeline, ordering, and incremental rules backed by the code.",
"createdAt": "2025-11-23T19:00:00+08:00",
"lastModified": "2025-11-23T19:40:52+08:00",
"order": "4"
}
},
{
path: '/builder/plugins',
component: Route6,
title: 'Plugins',
meta: {
title: 'Plugins',
description: 'Lifecycle hooks, authoring a custom plugin, and built-in plugins.',
createdAt: '2025-11-23T19:00:00+08:00',
lastModified: '2025-11-23T19:40:52+08:00',
order: '5',
},
"title": "Plugins",
"description": "Lifecycle hooks, authoring a custom plugin, and built-in plugins.",
"createdAt": "2025-11-23T19:00:00+08:00",
"lastModified": "2025-11-23T19:40:52+08:00",
"order": "5"
}
},
{
path: '/builder/built-ins',
component: Route7,
title: 'Built-in Plugins',
meta: {
title: 'Built-in Plugins',
description: 'Ready-to-use plugins shipped with the builder and how to configure them.',
createdAt: '2025-11-23T19:35:00+08:00',
lastModified: '2025-11-23T19:40:52+08:00',
order: '6',
},
"title": "Built-in Plugins",
"description": "Ready-to-use plugins shipped with the builder and how to configure them.",
"createdAt": "2025-11-23T19:35:00+08:00",
"lastModified": "2025-11-23T19:40:52+08:00",
"order": "6"
}
},
{
path: '/storage/providers',
component: Route8,
title: 'Storage Providers',
meta: {
title: 'Storage Providers',
description: 'Choose a storage provider for your photo collection.',
createdAt: '2025-11-14T22:40:00+08:00',
lastModified: '2025-11-23T19:40:52+08:00',
order: '30',
},
"title": "Storage Providers",
"description": "Choose a storage provider for your photo collection.",
"createdAt": "2025-11-14T22:40:00+08:00",
"lastModified": "2025-11-23T19:40:52+08:00",
"order": "30"
}
},
{
path: '/storage/providers/s3',
component: Route9,
title: 'S3 / S3-Compatible',
meta: {
title: 'S3 / S3-Compatible',
description: 'Configure S3 or S3-compatible storage for your photo collection.',
createdAt: '2025-11-14T22:10:00+08:00',
lastModified: '2025-11-23T19:40:52+08:00',
order: '32',
},
"title": "S3 / S3-Compatible",
"description": "Configure S3 or S3-compatible storage for your photo collection.",
"createdAt": "2025-11-14T22:10:00+08:00",
"lastModified": "2025-11-23T19:40:52+08:00",
"order": "32"
}
},
{
path: '/storage/providers/b2',
component: Route10,
title: 'B2 (Backblaze B2)',
meta: {
title: 'B2 (Backblaze B2)',
description: 'Configure Backblaze B2 storage for cost-effective cloud storage.',
createdAt: '2025-11-14T22:10:00+08:00',
lastModified: '2025-11-23T19:40:52+08:00',
order: '33',
},
"title": "B2 (Backblaze B2)",
"description": "Configure Backblaze B2 storage for cost-effective cloud storage.",
"createdAt": "2025-11-14T22:10:00+08:00",
"lastModified": "2025-11-23T19:40:52+08:00",
"order": "33"
}
},
{
path: '/storage/providers/github',
component: Route11,
title: 'GitHub Storage',
meta: {
title: 'GitHub Storage',
description: 'Use a GitHub repository as photo storage for simple deployments.',
createdAt: '2025-11-14T22:10:00+08:00',
lastModified: '2025-11-23T19:40:52+08:00',
order: '34',
},
"title": "GitHub Storage",
"description": "Use a GitHub repository as photo storage for simple deployments.",
"createdAt": "2025-11-14T22:10:00+08:00",
"lastModified": "2025-11-23T19:40:52+08:00",
"order": "34"
}
},
{
path: '/storage/providers/local',
component: Route12,
title: 'Local Storage',
meta: {
title: 'Local Storage',
description: 'Use local file system paths for development and self-hosting.',
createdAt: '2025-11-14T22:10:00+08:00',
lastModified: '2025-11-23T19:40:52+08:00',
order: '35',
},
"title": "Local Storage",
"description": "Use local file system paths for development and self-hosting.",
"createdAt": "2025-11-14T22:10:00+08:00",
"lastModified": "2025-11-23T19:40:52+08:00",
"order": "35"
}
},
{
path: '/storage/providers/eagle',
component: Route13,
title: 'Eagle Storage',
meta: {
title: 'Eagle Storage',
description: 'Publish directly from an Eagle 4 library with filtering support.',
createdAt: '2025-11-14T22:10:00+08:00',
lastModified: '2025-11-23T19:40:52+08:00',
order: '36',
},
"title": "Eagle Storage",
"description": "Publish directly from an Eagle 4 library with filtering support.",
"createdAt": "2025-11-14T22:10:00+08:00",
"lastModified": "2025-11-23T19:40:52+08:00",
"order": "36"
}
},
{
path: '/builder',
component: Route14,
title: 'Builder',
meta: {
title: 'Builder',
description: 'Overview of the photo pipeline and how to choose the right entrypoint for your run.',
createdAt: '2025-11-14T22:10:00+08:00',
lastModified: '2025-11-23T19:40:52+08:00',
order: '40',
},
"title": "Builder",
"description": "Overview of the photo pipeline and how to choose the right entrypoint for your run.",
"createdAt": "2025-11-14T22:10:00+08:00",
"lastModified": "2025-11-23T19:40:52+08:00",
"order": "40"
}
},
{
path: '/deployment',
component: Route15,
title: 'Deployment',
meta: {
title: 'Deployment',
description: 'Deploy your gallery to static hosting, SSR platforms, or Docker.',
createdAt: '2025-07-20T22:35:03+08:00',
lastModified: '2025-11-23T19:40:52+08:00',
order: '50',
},
"title": "Deployment",
"description": "Deploy your gallery to static hosting, SSR platforms, or Docker.",
"createdAt": "2025-07-20T22:35:03+08:00",
"lastModified": "2025-11-23T20:22:24+08:00",
"order": "50"
}
},
{
path: '/deployment/docker',
component: Route16,
title: 'Docker',
meta: {
title: 'Docker',
description: 'Deploy Afilmory using Docker for consistent, containerized deployments.',
createdAt: '2025-07-20T22:35:03+08:00',
lastModified: '2025-11-23T19:40:52+08:00',
order: '51',
},
"title": "Docker",
"description": "Deploy Afilmory using Docker for consistent, containerized deployments.",
"createdAt": "2025-07-20T22:35:03+08:00",
"lastModified": "2025-11-23T19:40:52+08:00",
"order": "51"
}
},
{
path: '/deployment/github-pages',
component: Route17,
title: 'GitHub Pages',
meta: {
title: 'GitHub Pages',
description: 'Deploy your gallery to GitHub Pages for free static hosting.',
createdAt: '2025-07-20T22:35:03+08:00',
lastModified: '2025-11-23T19:40:52+08:00',
order: '52',
},
"title": "GitHub Pages",
"description": "Deploy your gallery to GitHub Pages for free static hosting.",
"createdAt": "2025-07-20T22:35:03+08:00",
"lastModified": "2025-11-23T19:40:52+08:00",
"order": "52"
}
},
{
path: '/deployment/cloudflare-pages',
component: Route18,
title: 'Cloudflare Pages',
meta: {
title: 'Cloudflare Pages',
description: 'Deploy your gallery to Cloudflare Pages for fast global CDN distribution.',
createdAt: '2025-07-20T22:35:03+08:00',
lastModified: '2025-11-23T19:40:52+08:00',
order: '53',
},
"title": "Cloudflare Pages",
"description": "Deploy your gallery to Cloudflare Pages for fast global CDN distribution.",
"createdAt": "2025-07-20T22:35:03+08:00",
"lastModified": "2025-11-23T19:40:52+08:00",
"order": "53"
}
},
{
path: '/deployment/vercel',
component: Route19,
title: 'Vercel',
meta: {
title: 'Vercel',
description: 'Deploy your gallery to Vercel with server-side rendering support.',
createdAt: '2025-07-20T22:35:03+08:00',
lastModified: '2025-11-23T19:40:52+08:00',
order: '54',
},
"title": "Vercel",
"description": "Deploy your gallery to Vercel with server-side rendering support.",
"createdAt": "2025-07-20T22:35:03+08:00",
"lastModified": "2025-11-23T19:40:52+08:00",
"order": "54"
}
},
{
path: '/deployment/saas',
component: Route20,
title: 'Advanced SaaS Deployment',
meta: {
"title": "Advanced SaaS Deployment",
"description": "Run the full multi-tenant stack (API + OAuth gateway) with subdomains and custom domains.",
"createdAt": "2025-11-23T19:50:00+08:00",
"lastModified": "2025-11-23T20:22:24+08:00",
"order": "60"
}
},
{
path: '/docs-site',
component: Route20,
title: 'Docs site',
component: Route21,
title: 'Docs Site',
meta: {
title: 'Docs site',
description: 'Docs site introduction and contribution guide',
createdAt: '2025-09-11T17:05:55+08:00',
lastModified: '2025-11-23T19:40:52+08:00',
order: '90',
},
},
"title": "Docs Site",
"description": "Docs site introduction and contribution guide",
"createdAt": "2025-09-11T17:05:55+08:00",
"lastModified": "2025-11-23T20:22:24+08:00",
"order": "90"
}
}
]
export default routes

View File

@@ -453,6 +453,12 @@ export const tocData: FileToc[] = [
"level": 3,
"text": "Docker",
"children": []
},
{
"id": "heading-multi-tenant-saas",
"level": 3,
"text": "Multi-tenant SaaS",
"children": []
}
]
},
@@ -847,6 +853,99 @@ export const tocData: FileToc[] = [
}
]
},
{
"file": "deployment/saas.mdx",
"path": "/deployment/saas",
"title": "Advanced SaaS Deployment",
"toc": [
{
"id": "heading-advanced-saas-deployment",
"level": 1,
"text": "Advanced SaaS Deployment",
"children": [
{
"id": "heading-components",
"level": 2,
"text": "Components",
"children": []
},
{
"id": "heading-quick-start-with-docker-all-in-one-core-image",
"level": 2,
"text": "Quick start with Docker (all-in-one core image)",
"children": []
},
{
"id": "heading-first-login-flow",
"level": 2,
"text": "First-login flow",
"children": []
},
{
"id": "heading-domain-model",
"level": 2,
"text": "Domain model",
"children": []
},
{
"id": "heading-dns-layout",
"level": 2,
"text": "DNS layout",
"children": []
},
{
"id": "heading-runtime-configuration",
"level": 2,
"text": "Runtime configuration",
"children": [
{
"id": "heading-core-service-beappscore",
"level": 3,
"text": "Core service (`be/apps/core`)",
"children": []
},
{
"id": "heading-oauth-gateway-beappsoauth-gateway",
"level": 3,
"text": "OAuth Gateway (`be/apps/oauth-gateway`)",
"children": []
}
]
},
{
"id": "heading-request-routing-flow",
"level": 2,
"text": "Request routing flow",
"children": []
},
{
"id": "heading-tenant-lifecycle",
"level": 2,
"text": "Tenant lifecycle",
"children": []
},
{
"id": "heading-deployment-checklist",
"level": 2,
"text": "Deployment checklist",
"children": []
},
{
"id": "heading-licensing-notice-commercial-use",
"level": 2,
"text": "Licensing notice (commercial use)",
"children": []
},
{
"id": "heading-notes",
"level": 2,
"text": "Notes",
"children": []
}
]
}
]
},
{
"file": "deployment/vercel.mdx",
"path": "/deployment/vercel",
@@ -963,7 +1062,7 @@ export const tocData: FileToc[] = [
{
"file": "docs-site.mdx",
"path": "/docs-site",
"title": "Docs site",
"title": "Docs Site",
"toc": [
{
"id": "heading-afilmory-docs-site",