Skip to content

Review fixes on doc migration #138

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 82 commits into
base: v2
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
5c3f41d
chore: rm all
aheckmann Jul 28, 2025
741d761
chore: migrate to docusaurus
aheckmann Jul 28, 2025
5fde188
fix(vercel): wildcard redirect
aheckmann Jul 28, 2025
46c4326
chore: import latest changes
aheckmann Jul 28, 2025
d8ae380
fix: front-matter description spacing
aheckmann Jul 28, 2025
38aee93
fix: the 404s
aheckmann Jul 28, 2025
5be084a
chore: run prod locally
aheckmann Jul 28, 2025
6713266
chore: search styling
aheckmann Jul 28, 2025
6ad5425
chore: better search styles
aheckmann Jul 28, 2025
9fd4d5e
chore: write intro page
aheckmann Jul 29, 2025
9d46e84
chore: add old plane-one docs
aheckmann Jul 29, 2025
a7bd9d2
chore: format
aheckmann Jul 29, 2025
2583d36
chore: migrate dot files
aheckmann Jul 29, 2025
9fbdaa2
feat: add sdks
aheckmann Jul 29, 2025
cfc1066
chore(ci): use nvmrc
aheckmann Jul 29, 2025
2ae145c
feat: ai-solutions
aheckmann Jul 29, 2025
c2afef9
chore: standardize on "overview"
aheckmann Jul 29, 2025
d7bd809
chore: helm 1.3.1
aheckmann Jul 30, 2025
3451029
chore: refactor the webhooks guide
aheckmann Jul 30, 2025
2aea816
chore: fix up titles and footer links
aheckmann Jul 30, 2025
7503977
chore: clarify webhook verification
aheckmann Jul 30, 2025
092ca19
chore: pnpm version
aheckmann Jul 30, 2025
3c842c0
chore(build): corepack enable
aheckmann Jul 31, 2025
3eb1979
fix: img styles
aheckmann Jul 31, 2025
6deb4f6
chore: new primary colors
aheckmann Jul 31, 2025
d1dd9fb
fix(style): fix a11y color contrast
aheckmann Jul 31, 2025
600f5e1
fix: footer link icon
aheckmann Jul 31, 2025
b6f985b
chore: serve locally
aheckmann Jul 31, 2025
c6f494d
fix(react): use correct attr name
aheckmann Jul 31, 2025
70e33db
fix(style): navbar link color
aheckmann Jul 31, 2025
69027d1
fix: css issue in nav
aheckmann Jul 31, 2025
210c817
chore: rm all
aheckmann Jul 28, 2025
fe529b2
chore: migrate to docusaurus
aheckmann Jul 28, 2025
e14c360
fix(vercel): wildcard redirect
aheckmann Jul 28, 2025
d965feb
chore: import latest changes
aheckmann Jul 28, 2025
deaf7f8
fix: front-matter description spacing
aheckmann Jul 28, 2025
0ba291b
fix: the 404s
aheckmann Jul 28, 2025
5aa3294
chore: run prod locally
aheckmann Jul 28, 2025
886f694
chore: search styling
aheckmann Jul 28, 2025
17684a0
chore: better search styles
aheckmann Jul 28, 2025
46790e0
chore: write intro page
aheckmann Jul 29, 2025
c84becd
chore: add old plane-one docs
aheckmann Jul 29, 2025
e568bb7
chore: format
aheckmann Jul 29, 2025
95f3c2b
chore: migrate dot files
aheckmann Jul 29, 2025
2682977
feat: add sdks
aheckmann Jul 29, 2025
5c78163
chore(ci): use nvmrc
aheckmann Jul 29, 2025
6be56a4
feat: ai-solutions
aheckmann Jul 29, 2025
4d02d73
chore: standardize on "overview"
aheckmann Jul 29, 2025
bf978df
chore: helm 1.3.1
aheckmann Jul 30, 2025
bac3dd7
chore: refactor the webhooks guide
aheckmann Jul 30, 2025
5c9a0d3
chore: fix up titles and footer links
aheckmann Jul 30, 2025
3710d50
chore: clarify webhook verification
aheckmann Jul 30, 2025
b718756
chore: pnpm version
aheckmann Jul 30, 2025
5bc2f39
chore(build): corepack enable
aheckmann Jul 31, 2025
be40188
fix: img styles
aheckmann Jul 31, 2025
3aeee86
chore: new primary colors
aheckmann Jul 31, 2025
fad6cf6
fix(style): fix a11y color contrast
aheckmann Jul 31, 2025
029aeb6
fix: footer link icon
aheckmann Jul 31, 2025
bd73a04
chore: serve locally
aheckmann Jul 31, 2025
bba94ad
fix(react): use correct attr name
aheckmann Jul 31, 2025
25746ca
fix(style): navbar link color
aheckmann Jul 31, 2025
af06ed3
fix: css issue in nav
aheckmann Jul 31, 2025
1fda380
Github Enterprise Server - Create GitHub App
danciaclara Aug 1, 2025
7b18a80
chore: better api introduction
aheckmann Aug 1, 2025
30c5eb2
chore: re-enable interactive api examples
aheckmann Aug 1, 2025
faebd78
chore: fix colors for api docs
aheckmann Aug 1, 2025
fa625e0
chore(api): better lang highlighting
aheckmann Aug 1, 2025
88dc128
chore: improve openapi styles
aheckmann Aug 1, 2025
ec11454
feat: apps
aheckmann Aug 1, 2025
bc91d63
chore: styles
aheckmann Aug 1, 2025
c8d48bd
chore: refactor github app page
aheckmann Aug 1, 2025
fb13788
chore: rm old file
aheckmann Aug 2, 2025
bc7c386
Resolved merge conflicts
danciaclara Aug 12, 2025
6f74ce6
Fixed navbar and fonts
danciaclara Aug 18, 2025
fcec310
resolved merge conflicts
danciaclara Aug 18, 2025
d9b4c94
Updated page order for self-hosting
danciaclara Aug 18, 2025
54e5528
Updated Dev tools section
danciaclara Aug 18, 2025
d488395
minor edits
danciaclara Aug 18, 2025
a0c8a84
fixed broken link
danciaclara Aug 18, 2025
06e7465
removed duplicate schema.yaml
danciaclara Aug 19, 2025
f7d9d28
Restored original content on GitHub and Build Apps
danciaclara Aug 19, 2025
097d77b
fixed broken links
danciaclara Aug 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
379 changes: 379 additions & 0 deletions docs/apps/build-plane-app.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,379 @@
---
title: Build a Plane App (BYOA)
sidebar_label: App development
description: Step-by-step development guide to build and integrate an app with Plane using OAuth-based authentication and authorization workflow.

siderbar_position: 2
---

import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";

{frontMatter.description && (

<h3 class="description">{frontMatter.description}</h3>
)}

:::info
Plane apps are currently in **Beta**. Please send any feedback to [email protected].
:::

## Introduction
Plane apps seamlessly integrate tools and services with Plane so you can
use them without ever leaving your Workspace. Apps are conveniently available
from our [marketplace](https://plane.so/marketplace/integrations), helping you
stay focused and productive.

## Why Build a Plane App?

**Stop doing manual work.**
Plane integrations eliminate repetitive tasks like copying updates between
tools, creating work items from support tickets, and generating status reports.
Instead of spending hours on administrative work, let your app handle it
automatically.

**Connect everything you already use.**
Your team probably uses dozens of different tools. Plane apps create a unified
workflow by connecting your favorite CRM, time tracking app, CI/CD pipelines,
communication tools, and more, together into Plane. One change in Plane can
trigger updates across your entire tech stack.

**Build exactly what you need.**
Unlike rigid SaaS platforms, Plane's open core nature means you can create
integrations that fit your specific workflow.

## Prerequisites

- A [Plane](https://app.plane.so) workspace
- Admin access to your workspace settings
- Familiarity with OAuth 2.0 concepts (authorization code flow)
- A backend server to handle OAuth token exchange

## High-Level Workflow

1. [Register your app on Plane developer portal](/apps/build-plane-app/#registering-your-app)
2. [Implement OAuth flow](/apps/build-plane-app#implement-oauth-flow)
3. [Obtain and store access tokens securely](/apps/build-plane-app#obtain-and-store-access-tokens-securely)
4. [Make authenticated API requests to Plane](/apps/build-plane-app#make-authenticated-api-requests-to-plane)
5. [Handle token refresh](/apps/build-plane-app#handle-token-refresh)

## Registering Your App

To build an OAuth application with Plane:

1. Navigate to `https://app.plane.so/<workspace_slug>/settings/applications/`.
2. Click on the **Build your own** button.
3. Fill out the form with the required details:

- **Redirect URIs**: Provide the URIs where Plane will send the authorization code after the user consents to the app.
- **Contact Details**: Add your email or other contact information.
- **Setup URL(Optional)**: Provide the URL that users will be redirected to when they click "Install App" from the marketplace. This URL should initiate the OAuth flow for your application.
- **Webhook URL Endpoint(Optional)**: Your service's webhook endpoint. Plane will send an HTTP `POST` request to this endpoint upon every change to the workspace in which your app was installed.
- **Organization Details(Optional)**: Optionally include your contact email, privacy policy URL, terms of service URL, and any other relevant information. This helps Plane validate and approve your application should you choose to [list in the marketplace](#listing-your-app-on-plane-marketplace).

4. If you're building an agent (with or without using Plane's ADK) capable of performing operations when assigned or mentioned, enable the **Is Mentionable** checkbox during app creation.
5. Once the app is created, securely store the generated **Client ID** and **Client Secret**. You will need these credentials to interact with Plane's API during the OAuth flow and for making authenticated API requests.

## Implement OAuth Flow

### Generating Consent URL (Optional)

This step is optional. This is needed only if the app should be installed from outside Plane's environment, the developer needs to generate the consent URL using the client ID generated during their app creation flow.

If this flow needs to be triggered from Plane marketplace as well, then provide the URL in "Setup URL" field on application create screen to redirect the user from marketplace on clicking "Install App" button.

Below are sample implementations:

<Tabs queryString="lang" groupId="code-examples">
<TabItem label="Python" value="python" default>
```python
import os
from urllib.parse import urlencode
params = {
"client_id": os.getenv("PLANE_CLIENT_ID"),
"response_type": "code",
"redirect_uri": os.getenv("PLANE_REDIRECT_URI"),
}
consent_url = f"https://api.plane.so/auth/o/authorize-app/?{urlencode(params)}"
```
</TabItem>
<TabItem label="TypeScript" value="typescript">
```typescript
import { URLSearchParams } from 'url';
const params = new URLSearchParams({
client_id: process.env.PLANE_CLIENT_ID!,
response_type: "code",
redirect_uri: process.env.PLANE_REDIRECT_URI!,
});
const consentUrl = `https://api.plane.so/auth/o/authorize-app/?${params.toString()}`;
```
</TabItem>
</Tabs>

There are two types of authenticated actions your application can perform:

1. **User-authorized actions**: Actions performed on behalf of a user after they grant permission to your app via OAuth.
2. **App-authorized actions**: Actions that the app can perform independently within the workspace where it is installed (such as responding to webhooks or automation triggers).

For both these flows, Plane will make a GET request to the Redirect URI with parameters as mentioned in the following sections.

We will describe how to configure and use each type in the following sections.

### App-Authorized Actions (Client Credentials Flow)

When the app is installed, Plane will send an `app_installation_id` as part of the callback to the Redirect URI provided during consent URL generation. You can use this `app_installation_id` to request a bot token for your app.

Plane will make a GET request to the Redirect URI with below parameters:

| Parameter | Description |
|-----------|-------------|
| app_installation_id | The unique identifier for the app installation in the workspace |

#### Examples

<Tabs queryString="lang" groupId="code-examples">
<TabItem label="Python" value="python" default>
```python
import base64
import requests
client_id = "your_client_id"
client_secret = "your_client_secret"
basic_auth = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode()
payload = {
"grant_type": "client_credentials",
"app_installation_id": app_installation_id
}
response = requests.post(
url="https://api.plane.so/auth/o/token/",
headers={"Authorization": f"Basic {basic_auth}", "Content-Type": "application/x-www-form-urlencoded"},
data=payload
)
response_data = response.json()
bot_token = response_data['access_token']
expires_in = response_data["expires_in"]
```
</TabItem>
<TabItem label="TypeScript" value="typescript">
```typescript
import axios from 'axios';
const clientId = "your_client_id";
const clientSecret = "your_client_secret";
const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
const payload = {
grant_type: "client_credentials",
app_installation_id: appInstallationId
};
const response = await axios.post(
"https://api.plane.so/auth/o/token/",
payload,
{
headers: {
Authorization: `Basic ${basicAuth}`,
"Content-Type": "application/x-www-form-urlencoded"
}
}
);
const responseData = response.data;
const botToken = responseData.access_token;
const expiresIn = responseData.expires_in;
```
</TabItem>
</Tabs>

### User-Authorized Actions (Authorization Code Flow)

In this flow, your app exchanges the `code` received as a query parameter on the callback (to your Redirect URI) for an access token and refresh token. The access token is short-lived and must be refreshed using the refresh token when it expires. Both tokens should be securely stored.

Plane will make a GET request to the Redirect URI with below parameters:

| Parameter | Description | Required |
|-----------|-------------|----------|
| code | The authorization code that can be exchanged for an access token | Yes |
| state | The state parameter that was passed in the authorization request | No |

#### Examples

<Tabs queryString="lang" groupId="code-examples">
<TabItem label="Python" value="python" default>
```python
import requests
code = "authorization_code_from_callback"
client_id = "your_client_id"
client_secret = "your_client_secret"
redirect_uri = "your_redirect_uri"
payload = {
"grant_type": "authorization_code",
"code": code,
"client_id": client_id,
"client_secret": client_secret,
"redirect_uri": redirect_uri
}
response = requests.post(
url="https://api.plane.so/auth/o/token/",
headers={"Content-Type": "application/x-www-form-urlencoded"},
data=payload
)
response_data = response.json()
access_token = response_data["access_token"]
refresh_token = response_data["refresh_token"]
expires_in = response_data["expires_in"]
```
</TabItem>
<TabItem label="TypeScript" value="typescript">
```typescript
import axios from 'axios';
const code = "authorization_code_from_callback";
const clientId = "your_client_id";
const clientSecret = "your_client_secret";
const redirectUri = "your_redirect_uri";
const payload = {
grant_type: "authorization_code",
code: code,
client_id: clientId,
client_secret: clientSecret,
redirect_uri: redirectUri
};
const response = await axios.post(
"https://api.plane.so/auth/o/token/",
payload,
{
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
}
);
const responseData = response.data;
const accessToken = responseData.access_token;
const refreshToken = responseData.refresh_token;
const expiresIn = responseData.expires_in;
```
</TabItem>
</Tabs>

### Fetching App Installation Details

In both user-authorized and app-authorized flows, the `app_installation_id` identifies the app's installation within a specific workspace. It is recommended that developers fetch workspace details after OAuth is successfully completed. Plane provides an `app-installation` endpoint that works with both types of tokens.

#### Examples

<Tabs queryString="lang" groupId="code-examples">
<TabItem label="Python" value="python" default>
```python
import requests
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(
url=f"https://api.plane.so/auth/o/app-installation/?id={app_installation_id}",
headers=headers
)
workspace_details = response.data[0]
```
</TabItem>
<TabItem label="TypeScript" value="typescript">
```typescript
import axios from 'axios';
const headers = { Authorization: `Bearer ${token}` };
const response = await axios.get(
`https://api.plane.so/auth/o/app-installation/?id=${app_installation_id}`,
{ headers }
);
const workspaceDetails = response.data[0];
```
</TabItem>
</Tabs>

#### Sample Response

```json
[
{
"id": "34b97361-8636-43dc-953e-90deedc8498f",
"workspace_detail": {
"name": "sandbox",
"slug": "sandbox",
"id": "7a2e5944-c117-4a7d-b5f4-058fe705d7d1",
"logo_url": null
},
"created_at": "2025-05-16T13:50:27.865821Z",
"updated_at": "2025-06-23T08:57:26.976742Z",
"deleted_at": null,
"status": "installed",
"workspace": "7a2e5944-c117-4a7d-b5f4-058fe705d7d1",
"application": "ab235529-388a-4f51-a55a-78272251f5f1",
"installed_by": "63333ab1-c605-42fc-82f7-5cd86799eca1",
"app_bot": "7286aaa7-9250-4851-a520-29c904fd7654",
"webhook": "b1f4b7f1-51e8-4919-a84c-0b1143b51d2c"
}
]
```

## Obtain and store access tokens securely

Once you have obtained the access token, you can use it to make authenticated API requests to Plane.
Store the access token and refresh token securely in your database.


## Make authenticated API requests to Plane

For making authenticated API requests to Plane, you can use the access token obtained from the OAuth flow.

API reference is available at [https://docs.plane.so/api-reference](https://docs.plane.so/api-reference).

We have official SDKs for the following languages to simplify the OAuth flow and make it easier to call Plane's API.

| Language | Package Link | Source Code |
|----------|---------|-------------|
| Node.js | [npm i @makeplane/plane-node-sdk](https://www.npmjs.com/package/@makeplane/plane-node-sdk) | [plane-node-sdk](https://github.com/makeplane/plane-node-sdk) |
| Python | [pip install plane-sdk](https://pypi.org/project/plane-sdk/) | [plane-python-sdk](https://github.com/makeplane/plane-python-sdk) |

## Handle Token Refresh

When the access token expires, you can use the refresh token to get a new access token.

#### Examples

<Tabs queryString="lang" groupId="code-examples">
<TabItem label="Python" value="python" default>
```python
refresh_payload = {
"grant_type": "refresh_token",
"refresh_token": refresh_token,
"client_id": client_id,
"client_secret": client_secret
}
refresh_response = requests.post(
url="https://api.plane.so/auth/o/token/",
headers={"Content-Type": "application/x-www-form-urlencoded"},
data=refresh_payload
)
refresh_response_data = refresh_response.json()
access_token = refresh_response_data["access_token"]
```
</TabItem>
<TabItem label="TypeScript" value="typescript">
```typescript
const refreshPayload = {
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: clientId,
client_secret: clientSecret
};
const refreshResponse = await axios.post(
"https://api.plane.so/auth/o/token/",
refreshPayload,
{
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
}
);
const refreshResponseData = refreshResponse.data;
const accessToken = refreshResponseData.access_token;
```
</TabItem>
</Tabs>

## Listing Your App on Plane Marketplace

Apps built using the OAuth flow can be listed on the Plane Marketplace: [https://plane.so/marketplace/integrations](https://plane.so/marketplace/integrations)

To list your app, please contact the Plane team at [**[email protected]**](mailto:[email protected]).
2 changes: 1 addition & 1 deletion docs/apps/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ integrations that fit your specific workflow.

## Get Started

Next, follow our [quickstart guide](/apps/quickstart) to build your first Plane App.
Next, follow our [quickstart guide](/apps/build-plane-app) to build your first Plane App.
Loading