{"id":13632166,"url":"https://github.com/zenstackhq/saas-backend-template","last_synced_at":"2025-10-08T06:52:06.115Z","repository":{"id":176843620,"uuid":"656392804","full_name":"zenstackhq/saas-backend-template","owner":"zenstackhq","description":"ZenStack SaaS backend template","archived":false,"fork":false,"pushed_at":"2024-08-29T11:43:58.000Z","size":261,"stargazers_count":48,"open_issues_count":1,"forks_count":4,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-04T14:46:31.301Z","etag":null,"topics":["access-control","authorization","backend","expressjs","node","orm","restful","saas","schema-first","typescript","zenstack"],"latest_commit_sha":null,"homepage":"https://zenstack.dev/blog/saas-backend","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zenstackhq.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-06-20T21:34:40.000Z","updated_at":"2025-09-14T15:07:33.000Z","dependencies_parsed_at":null,"dependency_job_id":"e9f46b4f-fcc4-4995-b3d1-596a43bbc3d3","html_url":"https://github.com/zenstackhq/saas-backend-template","commit_stats":null,"previous_names":["zenstackhq/saas-backend-template"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/zenstackhq/saas-backend-template","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zenstackhq%2Fsaas-backend-template","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zenstackhq%2Fsaas-backend-template/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zenstackhq%2Fsaas-backend-template/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zenstackhq%2Fsaas-backend-template/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zenstackhq","download_url":"https://codeload.github.com/zenstackhq/saas-backend-template/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zenstackhq%2Fsaas-backend-template/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":278903006,"owners_count":26065786,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-08T02:00:06.501Z","response_time":56,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["access-control","authorization","backend","expressjs","node","orm","restful","saas","schema-first","typescript","zenstack"],"created_at":"2024-08-01T22:02:54.192Z","updated_at":"2025-10-08T06:52:06.100Z","avatar_url":"https://github.com/zenstackhq.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"# Please ⭐ us on the ZenStack repo if you like 🤝\n\nhttps://github.com/zenstackhq/zenstack\n\n# ZenStack SaaS Backend Template\n\nSaaS Backend Template using express.js\n\n## Features\n\n-   Multi-tenant\n-   Soft delete\n-   Sharing by group\n\n## Data Model\n\nIn `schema.zmodel,` there are 4 models, and their relationships are as below:\n![data model](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8dx12h1fiumotwhhxr7z.png)\n\n-   Organization is the top-level tenant. Any instance of User, post, and group belong to an organization.\n-   One user could belong to multiple organizations and groups\n-   One post belongs to a user and could belong to multiple groups.\n\n## Permissions\n\nLet’s take a look at all the permissions of the Post and how they could be expressed using ZenStack’s access policies.\n\n💡 _You can find the detailed reference of access policies syntax below:\n[https://zenstack.dev/docs/reference/zmodel-language#access-policy](https://zenstack.dev/docs/reference/zmodel-language#access-policy)_\n\n-   Create\n\nthe owner must be set to the current user, and the organization must be set to one that the current user belongs to.\n\n```tsx\n@@allow('create', owner == auth() \u0026\u0026 org.members?[id == auth().id])\n```\n\n-   Update\n\n    only the owner can update it and is not allowed to change the organization or owner\n\n    ```tsx\n    @@allow('update', owner == auth() \u0026\u0026 org.future().members?[id == auth().id] \u0026\u0026 future().owner == owner)\n    ```\n\n-   Read\n\n    -   allow the owner to read\n        ```tsx\n        @@allow('read', owner == auth())\n        ```\n    -   allow the member of the organization to read it if it’s public\n        ```tsx\n        @@allow('read', isPublic \u0026\u0026 org.members?[id == auth().id])\n        ```\n    -   allow the group members to read it\n        ```tsx\n        @@allow('read', groups?[users?[id == auth().id]])\n        ```\n\n-   Delete\n\n    -   don’t allow delete\n        The operation is not allowed by default if no rule is specified for it.\n    -   The record is treated as deleted if `isDeleted` is true, aka soft delete.\n        ```tsx\n        @@deny('all', isDeleted == true)\n        ```\n\nYou can see the complete data model together with the above access policies defined in the\n\n```tsx\nabstract model organizationBaseEntity {\n    id String @id @default(uuid())\n    createdAt DateTime @default(now())\n    updatedAt DateTime @updatedAt\n    isDeleted Boolean @default(false) @omit\n    isPublic Boolean @default(false)\n    owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)\n    ownerId String\n    org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)\n    orgId String\n    groups Group[]\n\n    // when create, owner must be set to current user, and user must be in the organization\n    @@allow('create', owner == auth() \u0026\u0026 org.members?[id == auth().id])\n    // only the owner can update it and is not allowed to change the owner\n    @@allow('update', owner == auth() \u0026\u0026 org.members?[id == auth().id] \u0026\u0026 future().owner == owner)\n    // allow owner to read\n    @@allow('read', owner == auth())\n    // allow shared group members to read it\n    @@allow('read', groups?[users?[id == auth().id]])\n    // allow organization to access if public\n    @@allow('read', isPublic \u0026\u0026 org.members?[id == auth().id])\n    // can not be read if deleted\n    @@deny('all', isDeleted == true)\n}\n\nmodel Post extends organizationBaseEntity {\n    title String\n    content String\n}\n```\n\n### Model Inheritance\n\nYou may be curious about why these rules are defined within the abstract `organizationBaseEntity` model rather than the specific **`Post`** model. That’s why I say it is **Scalable**. With ZenStack's model inheritance capability, all common permissions can be conveniently handled within the abstract base model.\n\nConsider the scenario where a newly hired developer needs to add a new **`ToDo`** model. He can effortlessly achieve this by simply extending the `organizationBaseEntity` :\n\n```tsx\nmodel ToDo extends organizationBaseEntity {\n    name String\n    isCompleted Boolean @default(false)\n}\n```\n\nAll the multi-tenant, soft delete and sharing features will just work automatically. Additionally, if any specialized access control logic is required for **`ToDo`**, such as allowing shared individuals to update it, you can effortlessly add the corresponding policy rule within the **`ToDo`** model without concerns about breaking existing functionality:\n\n```tsx\n@@allow('update', groups?[users?[id == auth().id]] )\n```\n\n## Running\n\n1. Install dependencies\n\n```bash\nnpm install\n```\n\n2. build\n\n```bash\nnpm run build\n```\n\n3. seed data\n\n```bash\nnpm run seed\n```\n\n4. start\n\n```bash\nnpm run dev\n```\n\n## Testing\n\nThe seed data is like below:\n\n![data](https://github.com/jiashengguo/my-blog-app/assets/16688722/6dfb2e8c-d1c3-4eec-8022-e03bf2dd42fd)\n\nSo in the Prisma team, each user created a post:\n\n-   **Join Discord** is not shared, so it could only be seen by Robin\n-   **Join Slack** is shared in the group to which Robin belongs so that it can be seen by both Robin and Bryan.\n-   **Follow Twitter** is a public one so that it could be seen by Robin, Bryan, and Gavin\n\nYou could simply call the Post endpoint to see the result simulate different users:\n\n```tsx\ncurl -H \"X-USER-ID: robin@prisma.io\" localhost:3000/api/post\n```\n\n💡 _It uses the plain text of the user id just for test convenience. In the real world, you should use a more secure way to pass IDs like JWT tokens._\n\nBased on the sample data, each user should see a different count of posts from 0 to 3.\n\n### Soft Delete\n\nSince it’s soft delete, the actual operation is to update `isDeleted` to true. Let’s delete the “Join Salck” post of Robin by running below:\n\n```tsx\ncurl -X PUT \\\n-H \"X-USER-ID: robin@prisma.io\" -H \"Content-Type: application/json\" \\\n-d '{\"data\":{ \"type\":\"post\", \"attributes\":{  \"isDeleted\": true } } }'\\\nlocalhost:3000/api/post/slack\n```\n\nAfter that, if you try to access the Post endpoint again, the result won’t contain the “Join Slack” post anymore. If you are interested in how it works under the hook, check out another post for it:\n\n[Soft Delete: Implementation Issues in Prisma and Solution in Zenstack](https://zenstack.dev/blog/soft-delete)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzenstackhq%2Fsaas-backend-template","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzenstackhq%2Fsaas-backend-template","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzenstackhq%2Fsaas-backend-template/lists"}