diff --git a/docs/docs/examples/imports/_setup_pylon.mdx b/docs/docs/examples/imports/_setup_pylon.mdx
new file mode 100644
index 0000000000..e68dad2c3c
--- /dev/null
+++ b/docs/docs/examples/imports/_setup_pylon.mdx
@@ -0,0 +1 @@
+You have to install Pylon as described in [their documentation](https://pylon.cronit.io/docs/installation/).
diff --git a/docs/docs/examples/secure-api/pylon.mdx b/docs/docs/examples/secure-api/pylon.mdx
new file mode 100644
index 0000000000..a607d12cd8
--- /dev/null
+++ b/docs/docs/examples/secure-api/pylon.mdx
@@ -0,0 +1,304 @@
+---
+title: ZITADEL with Pylon
+sidebar_label: Pylon
+---
+
+import AppJWT from "../imports/_app_jwt.mdx";
+import ServiceuserJWT from "../imports/_serviceuser_jwt.mdx";
+import ServiceuserRole from "../imports/_serviceuser_role.mdx";
+import SetupPylon from "../imports/_setup_pylon.mdx";
+
+This integration guide demonstrates the recommended way to incorporate ZITADEL into your Pylon service.
+It explains how to check the token validity in the API and how to check for permissions.
+
+By the end of this guide, your application will have three different endpoint which are public, private(valid token) and private-scoped(valid token with specific role).
+
+## ZITADEL setup
+
+Before we can start building our application, we have to do a few configuration steps in ZITADEL Console.
+
+### Create application
+
+
+
+### Create Serviceuser
+
+
+
+### Give Serviceuser an authorization
+
+
+
+### Prerequisites
+
+At the end you should have the following for the API:
+
+- Issuer, something like `https://example.zitadel.cloud` or `http://localhost:8080`
+- `.json`-key-file for the API, from the application
+- ID of the project
+
+And the following from the Serviceuser:
+
+- `.json`-key-file from the serviceuser
+
+## Setup new Pylon service
+
+### Setup Pylon
+
+
+
+### Creating a new project
+
+To create a new Pylon project, run the following command:
+
+```bash
+pylon new my-pylon-project
+```
+
+This will create a new directory called `my-pylon-project` with a basic Pylon project structure.
+
+### Project structure
+
+Pylon projects are structured as follows:
+
+```
+my-pylon-project/
+├── .pylon/
+├── src/
+│ ├── index.ts
+├── package.json
+├── tsconfig.json
+```
+
+- `.pylon/`: Contains the production build of your project.
+- `src/`: Contains the source code of your project.
+- `src/index.ts`: The entry point of your Pylon service.
+- `package.json`: The npm package configuration file.
+- `tsconfig.json`: The TypeScript configuration file.
+
+### Basic example
+
+Here's an example of a basic Pylon service:
+
+```ts
+import { defineService } from "@getcronit/pylon";
+
+export default defineService({
+ Query: {
+ sum: (a: number, b: number) => a + b,
+ },
+ Mutation: {
+ divide: (a: number, b: number) => a / b,
+ },
+});
+```
+
+## Secure the API
+
+### Add ZITADEL info to the service
+
+1. Create a `.env` file in the root folder of your project and add the following configuration:
+
+```bash
+AUTH_ISSUER='URL to the zitadel instance'
+AUTH_PROJECT_ID='ID of the project'
+```
+
+It should look something like this:
+
+```bash
+AUTH_ISSUER='https://example.zitadel.cloud'
+AUTH_PROJECT_ID='250719519163548112'
+```
+
+2. Copy the `.json`-key-file that you downloaded from the ZITADEL Console into the root folder of your project and rename it to `key.json`.
+
+### Auth
+
+Pylon provides a auth module and a decorator to check the validity of the token and the permissions.
+
+- `auth.initialize()`: Initializes the authentication middleware.
+- `auth.require()` : Middleware to check if the token is valid.
+- `auth.require({roles: ['role']})`: Middleware to check if the token is valid and has the specified roles.
+- `requireAuth()`: Decorator to check if the token is valid.
+- `requireAuth({roles: ['role']})`: Decorator to check if the token is valid and has the specified roles.
+
+### Build the Pylon service
+
+Now we will create a new Pylon service with the following endpoints:
+
+- `/api/public`: Public endpoint
+- `/api/private`: Private endpoint
+- `/api/private-scoped`: Private endpoint with specific role
+- `/graphql`: GraphQL endpoint
+ - Query: `me`: Private endpoint that returns the current user and the messages if the role is `read:messages`
+ - Query: `info`: Public endpoint
+
+### Create the service
+
+The following code demonstrates how to create a Pylon service with the required endpoints, it must be added to the `src/index.ts` file of your project:
+
+```ts
+import {
+ defineService,
+ PylonAPI,
+ auth,
+ requireAuth,
+ getContext,
+ ServiceError,
+} from "@getcronit/pylon";
+
+class User {
+ id: string;
+ name: string;
+ #messages: string[];
+
+ constructor(id: string, name: string, messages: string[]) {
+ this.id = id;
+ this.name = name;
+ this.#messages = messages;
+ }
+
+ @requireAuth({ roles: ["read:messages"] })
+ async messages() {
+ return this.#messages;
+ }
+
+ static users: User[] = [];
+
+ @requireAuth()
+ static async me() {
+ const ctx = getContext();
+ const id = ctx.get("auth")!.sub;
+
+ const user = User.users.find((user) => user.id === id);
+
+ if (!user) {
+ throw new ServiceError("User not found", {
+ statusCode: 404,
+ code: "USER_NOT_FOUND",
+ });
+ }
+
+ return user;
+ }
+
+ @requireAuth()
+ static async create() {
+ const ctx = getContext();
+
+ const auth = ctx.get("auth")!;
+
+ // Check if the user already exists
+
+ if (User.users.find((user) => user.id === auth.sub)) {
+ throw new ServiceError("User already exists", {
+ statusCode: 400,
+ code: "USER_ALREADY_EXISTS",
+ });
+ }
+
+ const user = new User(auth.sub, auth.username || "unknown", [
+ "Welcome to Pylon with ZITADEL!",
+ ]);
+
+ User.users.push(user);
+
+ return user;
+ }
+}
+
+export default defineService({
+ Query: {
+ me: User.me,
+ info: () => "Public Data",
+ },
+ Mutation: {
+ createUser: User.create,
+ },
+});
+
+export const configureApp: PylonAPI["configureApp"] = (app) => {
+ // Initialize the authentication middleware
+ app.use("*", auth.initialize());
+
+ // Automatically try to create a user for each request for demonstration purposes
+ app.use(async (_, next) => {
+ try {
+ await User.create();
+ } catch {
+ // Ignore errors
+ // Fail silently if the user already exists
+ }
+
+ await next();
+ });
+
+ app.get("/api/info", (c) => {
+ return new Response("Public Data");
+ });
+
+ // The `auth.require()` middleware is optional here, as the `User.me` method already checks for it.
+ app.get("/api/me", auth.require(), async (c) => {
+ const user = await User.me();
+
+ return c.json(user);
+ });
+
+ // A role check for `read:messages` is not required here, as the `user.messages` method already checks for it.
+ app.get("/api/me/messages", auth.require(), async (c) => {
+ const user = await User.me();
+
+ // This will throw an error if the user does not have the `read:messages` role
+ return c.json(await user.messages());
+ });
+};
+```
+
+### Call the API
+
+To call the API you need an access token, which is then verified by ZITADEL.
+Please follow [this guide here](/docs/guides/integrate/token-introspection/private-key-jwt#get-an-access-token), ignoring the first step as we already have the `.json`-key-file from the serviceaccount.
+
+:::info
+You can also create a PAT for the serviceuser and use it to test the API. For this, follow [this guide](/docs/guides/integrate/service-users/personal-access-token#create-a-service-user-with-a-pat).
+:::
+
+Optionally set the token as an environment variable:
+
+```
+export TOKEN='MtjHodGy4zxKylDOhg6kW90WeEQs2q...'
+```
+
+Now you have to start the Pylon service:
+
+```bash
+bun run develop
+```
+
+With the access token, you can then do the following calls:
+
+1. GraphQL:
+
+```
+curl -H "Authorization: Bearer $TOKEN" -G http://localhost:3000/graphql --data-urlencode 'query={ info }'
+curl -H "Authorization: Bearer $TOKEN" -G http://localhost:3000/graphql --data-urlencode 'query={ me { id name } }'
+curl -H "Authorization: Bearer $TOKEN" -G http://localhost:3000/graphql --data-urlencode 'query={ me { id name messages } }'
+
+```
+
+You can also visit the GraphQL playground at `http://localhost:3000/graphql` and execute the queries there.
+
+2. Routes:
+
+```
+curl -H "Authorization: Bearer $TOKEN" -X GET http://localhost:3000/api/info
+curl -H "Authorization: Bearer $TOKEN" -X GET http://localhost:3000/api/me
+curl -H "Authorization: Bearer $TOKEN" -X GET http://localhost:3000/api/me/messages
+```
+
+## Completion
+
+Congratulations! You have successfully integrated your Pylon with ZITADEL!
+
+If you get stuck, consider checking out their [documentation](https://pylon.cronit.io/). If you face issues, contact Pylon or raise an issue on [GitHub](https://github.com/getcronit/pylon/issues).
diff --git a/docs/frameworks.json b/docs/frameworks.json
index 0bf3e9d012..97afa11d47 100644
--- a/docs/frameworks.json
+++ b/docs/frameworks.json
@@ -111,5 +111,11 @@
"imgSrcDark": "/docs/img/tech/rustlight.svg",
"docsLink": "https://github.com/smartive/zitadel-rust",
"external": true
+ },
+ {
+ "title": "Pylon",
+ "imgSrcDark": "/docs/img/tech/pylon.svg",
+ "docsLink": "https://github.com/getcronit/pylon",
+ "external": true
}
]
diff --git a/docs/sidebars.js b/docs/sidebars.js
index 39f3d314d0..965a5d7a8c 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -38,6 +38,7 @@ module.exports = {
"examples/secure-api/python-django",
"examples/secure-api/python-flask",
"examples/secure-api/nodejs-nestjs",
+ "examples/secure-api/pylon",
{
type: "link",
label: ".Net",
@@ -100,6 +101,11 @@ module.exports = {
label: "Rust",
href: "https://github.com/smartive/zitadel-rust",
},
+ {
+ type: "link",
+ label: "Pylon",
+ href: "https://github.com/getcronit/pylon",
+ },
],
},
{
@@ -163,7 +169,7 @@ module.exports = {
"guides/manage/customize/user-schema",
],
},
- "guides/manage/terraform-provider"
+ "guides/manage/terraform-provider",
],
},
{
@@ -280,7 +286,7 @@ module.exports = {
label: "Service Users",
link: {
type: "doc",
- id: "guides/integrate/service-users/authenticate-service-users"
+ id: "guides/integrate/service-users/authenticate-service-users",
},
collapsed: true,
items: [
@@ -323,7 +329,10 @@ module.exports = {
{
type: "category",
label: "Login users with SSO",
- link: { type: "doc", id: "guides/integrate/identity-providers/introduction" },
+ link: {
+ type: "doc",
+ id: "guides/integrate/identity-providers/introduction",
+ },
collapsed: true,
items: [
"guides/integrate/identity-providers/google",
@@ -349,7 +358,7 @@ module.exports = {
label: "ZITADEL APIs",
link: {
type: "doc",
- id: "guides/integrate/zitadel-apis/access-zitadel-apis"
+ id: "guides/integrate/zitadel-apis/access-zitadel-apis",
},
collapsed: true,
items: [
@@ -478,9 +487,8 @@ module.exports = {
{
type: "autogenerated",
dirName: "concepts/structure",
- }
- ]
-
+ },
+ ],
},
{
type: "category",
@@ -490,9 +498,8 @@ module.exports = {
{
type: "autogenerated",
dirName: "concepts/features",
- }
- ]
-
+ },
+ ],
},
{
type: "autogenerated",
@@ -807,10 +814,7 @@ module.exports = {
type: "category",
label: "Actions V2",
collapsed: false,
- items: [
- "apis/actionsv2/introduction",
- "apis/actionsv2/execution-local",
- ],
+ items: ["apis/actionsv2/introduction", "apis/actionsv2/execution-local"],
},
{
type: "doc",
@@ -883,11 +887,9 @@ module.exports = {
collapsed: false,
link: {
type: "doc",
- id: "self-hosting/manage/cli/overview"
+ id: "self-hosting/manage/cli/overview",
},
- items: [
- "self-hosting/manage/cli/mirror"
- ],
+ items: ["self-hosting/manage/cli/mirror"],
},
],
},
diff --git a/docs/static/img/tech/pylon.svg b/docs/static/img/tech/pylon.svg
new file mode 100644
index 0000000000..e62fa6e281
--- /dev/null
+++ b/docs/static/img/tech/pylon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file