MIT License

技术解读

以下内容由 GitRead 中文社区 编辑团队基于项目 README 与官方资料整理, 结合中文开发者的常见使用场景补充技术选型建议。具体以项目官方文档为准。

ultimate-backend:把 SaaS 里那些“难活”先做一遍

项目背景与痛点

做 SaaS,真正卡住人的从来不是 CRUD。第一个租户接入时你写得很爽,第十个租户开始要独立角色体系,第一百个租户开始要按套餐限制功能、要审计日志、要能追溯“这封邮件是谁在什么权限下发出的”。这时候你才发现:身份、租户、订阅、事件流四条线绞在一起,改一处动全身。

市面上大多数 starter kit 给你的是模板:登录页、JWT、几张表。ultimate-backend 给的是架构约束——它把 NestJS、Apollo Federation、CQRS、Event Sourcing、Kafka、MongoDB 拼成一套可运行的分布式骨架,强制你在“命令—事件—投影”的链路上思考问题。它的价值不在代码本身,而在于替你提前踩过多租户 SaaS 的坑,并把坑的位置画在地图上。

核心技术揭秘

整体是典型的“网关 + 联邦子图 + 异步事件”三段式。GraphQL 层由 Apollo Federation 网关统一暴露 schema,auth、account、billing 各自作为 subgraph 独立部署、独立演进;写操作走 Command Bus 落到聚合根,产生领域事件写入事件存储,再由 Kafka 广播给投影器构建读模型,读请求命中物化视图。

graph TD C[客户端 Web / App] --> G[Apollo Federation 网关] G --> GUARD{"租户上下文校验"} GUARD -->|"通过"| ACCT[account 子图-租户与成员] GUARD -->|"拒绝"| ERR[返回 401 / 403] GUARD -->|"通过"| AUTH[auth 子图-认证与鉴权] G --> BILL[billing 子图-订阅计费] AUTH --> BUS[Command Bus] ACCT --> BUS BILL --> BUS BUS --> AGG[聚合根与命令处理器] AGG --> STORE[Event Store-事件流持久化] STORE --> MQ[Kafka 事件总线] MQ --> PROJ[投影器 Projector] PROJ --> RM[读模型-物化视图] RM --> G MQ --> SUB[GraphQL Subscription] SUB --> C

几个值得细看的设计点:

写读分离而非“两套代码”。命令处理器只负责校验不变量并追加事件,不直接改读模型;投影器是独立消费者,可以重放、可以重建。这意味着你可以新增一个读模型(比如“租户活跃度看板”)而不动业务逻辑——这是 CQRS 真正的红利。

多租户采用“共享集合 + tenantId”的软隔离。所有聚合与读模型都带租户维度,GraphQL context 中注入租户信息,Guard 在入口处完成归属校验。好处是运维简单、扩容线性;代价是索引设计和越权查询的风险全压在开发者身上,一条漏掉 tenantId 的查询就是数据泄漏。

跨服务一致性靠事件与补偿,而不是分布式事务。典型链路是:账户订阅成功 → 发事件 → billing 调 Stripe 创建订阅 → 成功回写状态,失败则发补偿事件回滚。这里没有两阶段提交,只有最终一致,因此读己之写(read-your-writes) 必须显式处理,否则前端刚下单就查不到订单。

认证层是“策略组合”而非硬编码。Passport 策略 + JWT 双令牌 + 租户级 RBAC,OAuth 登录与本地登录共享同一套 token 签发流程,权限判断下沉到装饰器与 Guard,业务代码里几乎看不到鉴权逻辑——这点做得相当克制。

功能亮点与差异

对比常见的 NestJS 脚手架,它的差异点很清晰:

  • 联邦 GraphQL 而非单体 schema:子图可独立部署,团队边界与 schema 边界一致,避免后期“一个大 GraphQL 变成新单体”。
  • 事件溯源是完整实现,不是花架子:事件持久化、重放、投影、订阅推送形成闭环,Subscription 通过 Redis PubSub 跨实例分发。
  • 计费内建:Stripe 订阅、套餐、额度限制被当作一等公民,而不是留给你“自己接一下”。
  • 多租户贯穿到底:从 token 到 context 到聚合根到读模型,租户维度没有断点。

真正稀缺的是第三、四点:绝大多数开源 SaaS 模板止步于“能登录”。

应用场景与落地建议

它最适合B2B 多租户 SaaS 的早期架构验证:团队 3~10 人、业务模型尚未完全固化、但已经确定要走订阅制与多服务拆分。拿它当“架构参考实现”读一遍,比当框架直接 fork 更划算。

上生产前务必确认几件事:

  1. Kafka 的运维成本。事件总线是整套架构的主动脉,一旦阻塞,写侧成功而读侧停滞,用户体验会诡异得像数据丢失。小团队可先用 NATS 或 RabbitMQ 过渡。
  2. 事件存储的选型。用 MongoDB 存事件流便于起步,但缺少专门的流式读取与订阅能力;事件量上来后,迁移到 EventStoreDB 或 Kafka 持久化主题会更稳。
  3. 事件版本与幂等。任何事件的字段变更都必须有 upcasting 方案,消费者必须幂等——重放是常态,不是异常。
  4. 隔离级别按业务分级。普通租户共享集合足够;涉及财务、医疗数据的租户,建议物理分库或至少分集合。
  5. 补齐可观测性。CQRS 架构下“一条命令为什么没生效”需要跨 Command、Event、Projection 三段追踪,OpenTelemetry + 事件 ID 贯穿是刚需。

综合评价

优点:架构野心与完成度在开源 SaaS 模板里属于上游;CQRS + Event Sourcing + Federation 的组合不是拼盘,而是彼此咬合的;多租户与计费的落地细节能省掉大量试错;代码组织清晰,适合当分布式 NestJS 的教科书读。

缺点:学习曲线陡,不熟悉事件溯源的开发者第一次读会迷失在 handler 与 projector 之间;项目迭代节奏偏慢,依赖版本需要自行升级;部分模块仍带演示性质,缺少生产级的重试、死信、监控;文档密度不足以覆盖所有设计决策,很多“为什么这么写”只能靠读代码反推。

一句话总结:它不是拿来即用的产品,而是一份带可运行代码的架构提案。想清楚自己要什么再 fork,否则你会继承它的复杂度,却拿不到它的收益。

关于

GitHub 原始描述(英文)

Multi tenant SaaS starter kit with cqrs graphql microservice architecture, apollo federation, event source and authentication

描述取自 GitHub 官方 API,保持仓库作者书写的原文。

语言占比

Dockerfile
HCL
Shell
TypeScript

README

ULTIMATE BACKEND

(WIP): This is an enterprise scale advanced microservice pattern with GraphQL API and GRPC Microservices, based on Domain (DDD) using the command query responsibility segregation (CQRS) design pattern. Want to ask Rex Isaac Raphael questions, join the slack channel :)

License Snyk Code Size Version Top Language Top Language

Description

Note: The current boilerplate repo is here WIP Ultimate Backend Boilerplate. This repo is currently houses the microservice packages Next Branch.

This should be the go to backend base for your next scalable project. This is a proof of concept project designed to be extremely slim and scalable, with distributed data request and process handling, built from the ground up for production use. It comes with Multi-Tenancy SaaS support, following different multi-tenancy database strategy as well as different resolver patterns
to identify your tenants. The goal is to give your next big project that extra leap to awesomeness. To get started read the instructions below. With support for both Event Store and NATS Streaming for event streaming and Kafka comming soon.

Note: Seeing alot of clone of the project which is good, but please if you can 🌟 the project as it also motivates me in improving the project. Also the docker azure CI setups is broken and will be fixed soon.

Note: Also ultimate backend is coming to rust as a complete microservice framework, if you want to be part of it and you program in rust, please write to me. Here is the repo ultimate

Features

Software features

  • ✅ CQRS
  • ✅ Software as a Service
  • ✅ Authentication by stateful session (Password) GraphQL
  • ✅ OAuth2 Authentication (Google, Github, Facebook) REST
  • ✅ User
  • ✅ Event Sourcing
  • ✅ GraphQL API
  • ✅ GRPC Microservice
  • ✅ Emailing Queue
  • ✅ Role Based Access Control
  • ✅ Multi Tenancy
  • ✅ Payment (Stripe)
  • ✅ SaaS Plans (Stripe)
  • ✅ Security
  • ✅ Service Discovery (Default on Consul), supports ectd, Kubernetes
  • ✅ React SSR Starter Kit
  • ❌ (WiP) Documentation
  • ✅ (WiP) Webhooks
  • ❌ (WiP) Support for language translation
  • ❌ (WiP) GraphQL dataloaders

Software stack

Required Optional
Store and cache Event Store (Event Source Store), Redis (Queue & cache) and MongoDB (Database) ArangoDB (Database), NATS Streaming (Event Source Store)
Stack and frameworks NestJS (Server Framework), NodeJS (System runtime), Typescript, Express JS, Fastify, GRPC, NestCloud and Apollo GraphQL none
Deployment and containerization Docker Kubernetes, Azure Pipeline, GitLab CI
Service Registry Consul Kubernetes and etcd

Folder Structure

Senior candidate in the folder structure is the /app folder. This folder contains all executable programs or in this case microservices

  • /app/service-access The access microservice handles access token management for each tenant in the system. It also validates current tenant credentials against a tenant specific resources. It is the gate keeper for your tenant api.
  • /app/service-account The account microservice handles user account commands and queries such as creating and validating new and current users in the system.
  • /app/service-tenant The tenant microservice handles creating new tenants as well as managing tenant specific commands and queries.
  • /app/service-role Similar to service-access, this service validates users based on roles against the entire system and tenants they belong to. This service handles only user authorization, where service-access is for tenant access keys for external api.
  • /app/service-notification The microservice performs notification tasks in the entire systems. Right now it supports emailing, but can be extended to support push notification as well as activity feeds.
  • /app/service-billing The billing microservice manages billing and payment commands. It is tightly integrated with Stripe, but can be easily replaced.
  • /app/service-project This microservice is merely an example of supporting multi-tenant database strategy. It holds no other significance.

The next important folder is the /lib folder. This folder contains all internal libraries that is depended upon by the microservices, and they are,

  • /lib/common House shared/common modules in the project.
  • /lib/core This is the core module of ultimate backend and houses most of the codebase that wires everything together.
  • /lib/repo-orm This library will be extracted in the future as it matures. It is aimed at being a simple, less cluttered NoSQL multi-tenant object relational mapper (ORM), It supports caching, MongoDB and ArangoDB, with support for FaunaDB in the works.
  • /lib/contracts Shared contracts for both typescript typings and graphql code first types, are stored here.
  • /lib/repository It holds all repositories used in the microservices, and this repository and created from the repo-rom library.
  • /lib/proto-schema All GRPC protobuf files required by each service, is stored in here and shared amongst services that depends on them.

Other folders such as iac which simply means Infrastructure as Code is teraform setup for creating a kubernetes cluster with just a simple command for running the microservices. scripts contains helper bash scripts for CI/CD.

Installation

$ yarn install

Configuration

Before starting the services, please create the appropriate consul (Default service registry Consul) kv store config for all the services. You can find the example config
in the folders of each service called config.example. The consul config key of say the account service should be
ultimatebackend/config/io.ultimatebackend.srv.account and just paste the config.yaml content in the consul store for that key in yaml and save.
You will need to set your sendgrid api key, so the backend can send emails on signup etc. If using stripe for payments you’ll also need to put your public and private keys there too.
You can opt in for etcd or kubernetes as service registry.

Usage

With Docker locally

$ docker-compose --project-directory=. -f docker-compose.dev.yml up --build

Note: I’ve seen some issues with consul docker image and so would recommend setting up consul manually before running this command

Without Docker locally

Consul, Mongodb, redis, memcached, and eventstore all need to be started first as our microservices need to connect to them.

Start consul locally

consul agent --dev  

For help installing consul on your local machine, visit Consul Website

Start mongodb locally

mongod  

If you have docker installed

docker run -d -p 27017:27017 mongo  
docker run -d -p 1113:1113 -p 2113:2113 eventstore/eventstore --insecure # insecure flag specifies no certificate required - suitable for devmode 
docker run -d -p 6379:6379 redis  

Otherwise, you can install and run redis and eventstore locally if you choose.

Running the microservices

You should start the microservices in any other. Example

  
# Generate protobuf typescript definitions and please fix the path for timestamps import (You should do this only when you update the protobuf files)
# and also build the proto-scheme lib (You should do this only when you update the protobuf files)
# in `lib/proto-schem`
# Also adds configuration(config.example files) to consul K/V registry (consul need to be running)
# You need jq and yq programs to use it   
$ yarn setup:local
  
# Start the account service  
$ npx nest start service-account  
  
# Start the access service  
$ npx nest start service-access  
  
# Start the role service  
$ npx nest start service-role  
  
# Start the graphql api  
$ npx nest start api-admin  
  

Note: You don’t need all services running plus you can start them in any order.

Alternative method of running services

If you find the nest cli using too much memory running each service, you can build them first and then run them:

Build each service:

npx nest build service-account  
npx nest build service-notification  
npx nest build service-billing  
npx nest build service-project  
npx nest build service-tenant  
npx nest build service-access  
npx nest build service-role  

Each service is built and written into dist/apps directory from where you can directly run each service with nodejs. Running each service with npx nest start uses three orders of magnitude more memory than this method so you will use a lot less memory!

Run each service in a separate terminal:

node dist/apps/service-account/main.js  
node dist/apps/service-notification/main.js  
node dist/apps/service-billing/main.js  
node dist/apps/service-project/main.js  
node dist/apps/service-tenant/main.js  
node dist/apps/service-access/main.js  

With the databases and the services running you can now start the gateways as mentioned above.

yarn start:dev api-admin  

Get started by registering a user on the admin gateway

In the graphql playground running at http://localhost:4000//graph you can register a user:

mutation register {  
  account {  
    register(input:{  
        firstname: "Alice"  
        lastname: "Bob"  
        email: "[email protected]"  
        password: "supersecretpassword"  
    }) {  
      token  
    }  
  }  
}  

All going well you should have received the following reply as well as an email with a verification code as well as a jwt token that contains both email and verification code for your frontend

{  
    "data": {  
        "account": {  
            "register": {  
                "token": "gtdjghdrtd65edjhc-chxgfxjtgzrkzxyzxtrs45wi6kydch"  
            }  
        }  
    }  
}  

Quick Tips

Generating dynamic mongo filter GraphQL type.

You can generate dynamic graphql filter by decorating the fields you want to be
avaiable in your generated filter type with the @Filterable() decorator like below

import { Filterable } from '@ultimatebackend/core';  
  
@ObjectType()  
export class Tenant extends Node {  
  
  @Filterable()  
  @Field({ nullable: true })  
  name: string;  
  
  @Filterable()  
  @Field({ nullable: true })  
  normalizedName: string;  
}  

After adding decorator to the fields you can now add generate your graphql input type.

Note: Enums and sub-types not supported at the moment.

import { FilterMongo } from '@ultimatebackend/contracts';  
  
@InputType()  
export class TenantFilterInput extends FilterMongo(Tenant, { simple: true }) {}  
Multi-tenant Database for services.

You can enable multi-tenant database support in your micro-service by adding this

MongoModule.registerAsync({  
 useClass: MongoMultiTenantConfigService,  
})

code block to the service app module. For example;

import { Module } from '@nestjs/common';  
import { MongoModule } from '@juicycleff/repo-orm/database';  
import { MongoMultiTenantConfigService } from '@ultimatebackend/core/mutiltenancy';  
  
@Module({  
  imports: [
    // ...
    MongoModule.registerAsync({  
      useClass: MongoMultiTenantConfigService,  
    }),
    // ...
  ],  
})  
export class AppModule {}  

A good example is the service-project microservice.

Next you must enable multi-tenancy in the main.ts file of the api-admin service or any other api type microservice you create in your project.

import { NestFactory } from '@nestjs/core';  
import { NestCloud } from '@nestcloud/core';  
import { AppModule } from './app.module';  
import { enableMultiTenancy, TenantDatabaseStrategy } from '@ultimatebackend/core/mutiltenancy';  
  
async function bootstrap() {  
  const app = NestCloud.create(await NestFactory.create(AppModule));  
  
  app.use(enableMultiTenancy({  
    enabled: true,  
    tenantResolver: {  
      resolverType: 'Header',  
      headerKeys: {  
        tenant: 'x-tenant-id',  
        apiKey: 'x-tenant-key',  
      },  
      requiresToken: true,  
    },  
    databaseStrategy: TenantDatabaseStrategy.DataIsolation,  
  }));  
  
  // ..... code continues  
}  
  
Access token with scopes

Access tokens scopes just a combination of action and resource identifier. For example, take this mutation;

  @UseGuards(GqlAuthGuard)
  @Resource({ name: 'billing', identify: 'billing:card', roles: ['customer'], action: 'update' })
  @ResolveField(() => Card)
  async create(@Args('input') input: CreateCardInput, @Context() ctx: GqlContext): Promise<Card> {
    // @ts-ignore
    const result = await this.service.billing.createCard({...input}, setRpcContext(ctx)).toPromise();
    return result.card;
  }

Your access token scope will be update_billing:card and so your mutation to create an access token should look like this

mutation {
  accessToken {
    create(input: {
      name: "my-superb-token",
      scopes: ["update_billing:card"]
    }) {
      token
      id
      name
      active
    }
  }
}

That’s all you need to know when creating access token which should be used together with your tenant normalized name to access the API without a logged in user.

More docs updates coming.

Test

# unit tests  
$ yarn run test  
  
# e2e tests  
$ yarn run test:e2e  
  
# test coverage  
$ yarn run test:cov  

Financial Contributors

Become a financial contributor. Your funds go to; people in need [Contribute]

Backers

Sponsors

Support this project with your organization. Your logo will show up here with a link to your website. [Contribute]

My Other Projects

Here are some of my projects, show some love and start them if you find them helpful :)

Special Thanks

This project wouldn’t be possible without these two awesome projects, NestJS (Server Framework) and NestCloud, please make sure to Star them.

License

This project is MIT licensed.

评论

暂无评论