mirror of
https://github.com/Afilmory/afilmory
synced 2026-02-01 22:48:17 +00:00
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:
53
.github/workflows/core-image.yml
vendored
Normal file
53
.github/workflows/core-image.yml
vendored
Normal 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 }}
|
||||
@@ -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
|
||||
|
||||
160
apps/docs/contents/deployment/saas.mdx
Normal file
160
apps/docs/contents/deployment/saas.mdx
Normal 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.
|
||||
@@ -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
|
||||
|
||||
@@ -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" /> {/* 平衡按钮 */}
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user