Integration Patterns for the Plane API: A Complete Guide to Third-Party Connections
Plane’s backend implements a three-layer Django/DRF architecture that exposes REST endpoints for provider registries, workspace-specific OAuth connections, and webhook handling to enable bidirectional sync with external services like Slack, GitHub, and GitLab.
The Integration Patterns for the Plane API define how external services connect to workspace data in the makeplane/plane repository. Built on Django REST Framework, the system separates provider metadata, per-workspace authentication tokens, and provider-specific extensions into distinct models that support scalable third-party integrations.
Core Integration Architecture
Plane’s integration layer organizes data into three distinct tiers: a global provider registry, workspace-specific connection records, and specialized models for individual services.
The Integration Registry
The Integration model in apps/api/plane/db/models/integration/base.py serves as the central registry for all supported third-party providers. This table stores static metadata about each service, including webhook URLs, redirect URLs, and verification status.
class Integration(AuditModel):
id = models.UUIDField(...)
title = models.CharField(max_length=400)
provider = models.CharField(max_length=400, unique=True) # e.g. "slack", "github"
network = models.PositiveIntegerField(choices=((1, "Private"), (2, "Public")))
description = models.JSONField(default=dict)
webhook_url = models.TextField(blank=True)
webhook_secret = models.TextField(blank=True)
redirect_url = models.TextField(blank=True)
metadata = models.JSONField(default=dict)
verified = models.BooleanField(default=False)
avatar_url = models.TextField(blank=True, null=True)
Adding a new provider requires inserting a row into this table with a unique provider string and relevant static URLs. The verified flag indicates whether the integration has completed platform-specific validation.
Per-Workspace Connections
The WorkspaceIntegration model establishes the link between a specific workspace, a user actor, and the third-party provider. Located in the same base.py file, this model enforces a one-connection-per-provider constraint through unique_together = ["workspace", "integration"].
class WorkspaceIntegration(BaseModel):
workspace = models.ForeignKey("db.Workspace", related_name="workspace_integrations", on_delete=models.CASCADE)
actor = models.ForeignKey("db.User", related_name="integrations", on_delete=models.CASCADE)
integration = models.ForeignKey("db.Integration", related_name="integrated_workspaces", on_delete=models.CASCADE)
api_token = models.ForeignKey("db.APIToken", related_name="integrations", on_delete=models.CASCADE)
metadata = models.JSONField(default=dict)
config = models.JSONField(default=dict)
The api_token field stores the OAuth token returned during the authorization flow, while the config JSON field holds provider-specific settings such as Slack channel IDs or repository mappings.
Provider-Specific Extensions
Each supported service extends the base pattern through dedicated models in apps/api/plane/db/models/integration/. These files wrap the generic WorkspaceIntegration to expose service-specific fields required for API operations.
For example, the Slack integration model in slack.py adds fields for access tokens and bot identification:
class ISlackIntegration(models.Model):
id = models.CharField(max_length=255, primary_key=True)
access_token = models.CharField(max_length=255)
bot_user_id = models.CharField(max_length=255)
webhook_url = models.URLField()
data = models.JSONField() # payload returned by Slack’s OAuth flow
# ... plus foreign keys to Workspace and WorkspaceIntegration
Similar extension files exist for GitHub (github.py) and GitLab (gitlab.py), following the same structural pattern of augmenting the base WorkspaceIntegration with provider-specific data.
REST API Surface and Webhook Handling
The API exposes standard CRUD endpoints for managing integrations and processing incoming webhooks. These routes are implemented in the DRF viewsets under apps/api/plane/api/views/.
| Endpoint | Method | Purpose |
|---|---|---|
/api/v1/integrations/ |
GET |
List available providers (Integration objects) |
/api/v1/workspace-integrations/ |
GET |
List active workspace connections |
/api/v1/workspace-integrations/ |
POST |
Create new connection with integration ID and api_token |
/api/v1/workspace-integrations/{id}/ |
PATCH/DELETE |
Update configuration or remove connection |
/api/v1/webhooks/ |
POST |
Receive and validate webhook events |
The webhook endpoint validates incoming requests against the webhook_secret stored in the Integration model. The OpenAPI schema documentation, generated by drf-spectacular, pulls parameter definitions from apps/api/plane/utils/openapi/parameters.py:
integration_source = openapi.Parameter(
name="integration_source",
location=openapi.IN_QUERY,
description="GitHub integration source",
type=openapi.TYPE_STRING,
)
External-ID Tracking for Bidirectional Sync
To maintain sync between Plane and external systems, core models like issues and assets include external_id and external_source fields. These fields enable webhook handlers to locate the corresponding Plane work items when processing updates from third-party services.
The issue serializer in apps/api/plane/api/serializers/issue.py exposes these fields:
external_id = serializers.CharField(required=False, help_text="External identifier for the issue (for integration tracking)")
external_source = serializers.CharField(required=False, help_text="External source system (for integration tracking)")
When importing data from GitHub or Slack, populate these fields with the provider’s native identifiers. This creates a reliable mapping that webhook handlers use to apply updates to the correct Plane entities.
Typical Integration Flow
Implementing a complete integration follows these sequential steps:
- Discover providers – Query
GET /api/v1/integrations/to retrieve available services. - OAuth handshake – Redirect the user to the provider’s authorization URL using the
redirect_urlfrom theIntegrationrecord. - Store credentials – Create an
APITokenwith the OAuth response, then POST to/api/v1/workspace-integrations/with the token ID and provider configuration. - Import with external IDs – When creating Plane work items from the provider, populate
external_idandexternal_sourceto establish bidirectional links. - Process webhooks – Listen for incoming events at
/api/v1/webhooks/and update work items by matching their external identifiers.
Adding a New Integration Provider
To extend Plane with a new provider like Asana:
- Seed the registry – Insert a row into the
Integrationtable with a uniqueproviderstring and appropriate webhook URLs. - Create extension model – Add a new file in
apps/api/plane/db/models/integration/defining provider-specific fields if the genericconfigJSON is insufficient. - Expose API fields – Update relevant serializers in
apps/api/plane/api/serializers/to include the new integration’s configuration options. - Implement webhook handling – Add view logic to validate secrets and process payload formats specific to the new service.
- Document parameters – Add parameter definitions to
apps/api/plane/utils/openapi/parameters.pyfor OpenAPI schema generation.
Summary
- Integration registry – The
Integrationmodel inapps/api/plane/db/models/integration/base.pystores metadata for all supported providers. - Workspace connections –
WorkspaceIntegrationlinks workspaces to providers via OAuth tokens stored inapi_tokenfields. - Provider extensions – Service-specific models like
ISlackIntegrationinslack.pyextend the base pattern for unique API requirements. - External ID tracking – Fields
external_idandexternal_sourceon work items enable bidirectional synchronization with third-party systems. - Webhook validation – Incoming events are validated against
webhook_secretstored in the provider registry.
Frequently Asked Questions
How does Plane handle authentication for third-party integrations?
Plane stores OAuth tokens in the APIToken model and references them through the WorkspaceIntegration.api_token foreign key. When a user connects a service, the system creates a new WorkspaceIntegration record linking the workspace, user, provider, and token, ensuring scoped access control.
What is the purpose of the external_id field on Plane issues?
The external_id field stores the native identifier from a third-party system (such as a GitHub issue number). This allows webhook handlers to locate and update the correct Plane work item when receiving change notifications from external services, maintaining synchronization between systems.
Can a workspace connect to multiple instances of the same provider?
No. The unique_together = ["workspace", "integration"] constraint on the WorkspaceIntegration model enforces one active connection per provider per workspace. To support multiple instances (e.g., two GitHub organizations), the config JSON field can store multiple authentication contexts, though this requires custom implementation.
Where are the TypeScript types for integrations defined?
Frontend type definitions reside in packages/types/src/integration.ts, which exports interfaces like IAppIntegration, IWorkspaceIntegration, and ISlackIntegration. These types mirror the Django model structure and ensure type safety across the Plane client application.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →