diff --git a/apps/console/src/pages/organizations/access-reviews/dialogs/AddAccessSourceDialog.tsx b/apps/console/src/pages/organizations/access-reviews/dialogs/AddAccessSourceDialog.tsx
index ee5d9bf77..c60291ae7 100644
--- a/apps/console/src/pages/organizations/access-reviews/dialogs/AddAccessSourceDialog.tsx
+++ b/apps/console/src/pages/organizations/access-reviews/dialogs/AddAccessSourceDialog.tsx
@@ -148,6 +148,9 @@ function mapAPIKeyExtraSettingToField(
case "OKTA":
if (settingKey === "domain") return "oktaDomain";
break;
+ case "BETTER_STACK":
+ if (settingKey === "teamName") return "betterStackTeamName";
+ break;
}
return null;
}
diff --git a/packages/ui/src/Atoms/ThirdParties/BetterStack.tsx b/packages/ui/src/Atoms/ThirdParties/BetterStack.tsx
new file mode 100644
index 000000000..4786b6da1
--- /dev/null
+++ b/packages/ui/src/Atoms/ThirdParties/BetterStack.tsx
@@ -0,0 +1,16 @@
+import type { ComponentProps } from "react";
+
+export function BetterStack(props: ComponentProps<"svg">) {
+ return (
+
+ );
+}
diff --git a/packages/ui/src/Atoms/ThirdParties/ThirdPartyLogo.tsx b/packages/ui/src/Atoms/ThirdParties/ThirdPartyLogo.tsx
index 333a8b8a6..a22681ac5 100644
--- a/packages/ui/src/Atoms/ThirdParties/ThirdPartyLogo.tsx
+++ b/packages/ui/src/Atoms/ThirdParties/ThirdPartyLogo.tsx
@@ -16,6 +16,7 @@ import type { ComponentProps, FC } from "react";
import { Anthropic } from "./Anthropic";
import { Asana } from "./Asana";
+import { BetterStack } from "./BetterStack";
import { Bitbucket } from "./Bitbucket";
import { Brex } from "./Brex";
import { Clerk } from "./Clerk";
@@ -57,6 +58,7 @@ import { Zendesk } from "./Zendesk";
const thirdParties: Record>> = {
ANTHROPIC: Anthropic,
ASANA: Asana,
+ BETTER_STACK: BetterStack,
BITBUCKET: Bitbucket,
BREX: Brex,
CLERK: Clerk,
diff --git a/packages/ui/src/Atoms/ThirdParties/index.ts b/packages/ui/src/Atoms/ThirdParties/index.ts
index 5a11c3007..ba7c824da 100644
--- a/packages/ui/src/Atoms/ThirdParties/index.ts
+++ b/packages/ui/src/Atoms/ThirdParties/index.ts
@@ -1,5 +1,6 @@
export { Anthropic } from "./Anthropic";
export { Asana } from "./Asana";
+export { BetterStack } from "./BetterStack";
export { Bitbucket } from "./Bitbucket";
export { Brex } from "./Brex";
export { Clerk } from "./Clerk";
diff --git a/pkg/accessreview/drivers/better_stack.go b/pkg/accessreview/drivers/better_stack.go
new file mode 100644
index 000000000..30f7da891
--- /dev/null
+++ b/pkg/accessreview/drivers/better_stack.go
@@ -0,0 +1,225 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+package drivers
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+
+ "go.probo.inc/probo/pkg/coredata"
+)
+
+// betterStackTeamMembersEndpoint is the Better Stack Uptime API team-members
+// resource. The team-members reference documents this apex host and returns
+// its pagination links on the same host, so the driver pins every page
+// request to it (instead of following the response's `next` URL) to avoid a
+// cross-host redirect that would drop the Authorization header.
+const betterStackTeamMembersEndpoint = "https://betterstack.com/api/v2/team-members"
+
+// BetterStackDriver fetches team members and pending invitations from the
+// Better Stack Uptime API via Bearer token-authenticated REST requests. The
+// teamName scopes the listing; it is required when authenticating with a
+// global API token and ignored for team-scoped tokens.
+type BetterStackDriver struct {
+ httpClient *http.Client
+ teamName string
+}
+
+var _ Driver = (*BetterStackDriver)(nil)
+
+type betterStackTeamMembersResponse struct {
+ Data []struct {
+ ID string `json:"id"`
+ Type string `json:"type"`
+ Attributes struct {
+ Email string `json:"email"`
+ FirstName string `json:"first_name"`
+ LastName string `json:"last_name"`
+ CreatedAt string `json:"created_at"`
+ InvitedAt string `json:"invited_at"`
+ Role string `json:"role"`
+ } `json:"attributes"`
+ } `json:"data"`
+ Pagination struct {
+ Next *string `json:"next"`
+ } `json:"pagination"`
+}
+
+func NewBetterStackDriver(httpClient *http.Client, teamName string) *BetterStackDriver {
+ return &BetterStackDriver{
+ httpClient: &http.Client{
+ Transport: &retryRoundTripper{
+ next: httpClient.Transport,
+ maxRetries: 3,
+ },
+ },
+ teamName: teamName,
+ }
+}
+
+func (d *BetterStackDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error) {
+ var records []AccountRecord
+
+ page := 1
+
+ for range maxPaginationPages {
+ resp, err := d.fetchTeamMembersPage(ctx, page)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, member := range resp.Data {
+ if member.Attributes.Email == "" {
+ continue
+ }
+
+ record := AccountRecord{
+ Email: member.Attributes.Email,
+ FullName: strings.TrimSpace(member.Attributes.FirstName + " " + member.Attributes.LastName),
+ Role: betterStackRole(member.Attributes.Role),
+ Active: betterStackActive(member.Type),
+ IsAdmin: betterStackIsAdmin(member.Attributes.Role),
+ MFAStatus: coredata.MFAStatusUnknown,
+ AuthMethod: coredata.AccessEntryAuthMethodUnknown,
+ AccountType: coredata.AccessEntryAccountTypeUser,
+ ExternalID: member.ID,
+ }
+
+ if t, ok := parseBetterStackTimestamp(member.Attributes.CreatedAt); ok {
+ record.CreatedAt = &t
+ } else if t, ok := parseBetterStackTimestamp(member.Attributes.InvitedAt); ok {
+ // Invitations carry invited_at but no created_at; keep the
+ // first-seen timestamp in CreatedAt for review context.
+ record.CreatedAt = &t
+ }
+
+ records = append(records, record)
+ }
+
+ if resp.Pagination.Next == nil || *resp.Pagination.Next == "" {
+ return records, nil
+ }
+
+ page++
+ }
+
+ return nil, fmt.Errorf("cannot list all better stack team members: %w", ErrPaginationLimitReached)
+}
+
+func (d *BetterStackDriver) fetchTeamMembersPage(
+ ctx context.Context,
+ page int,
+) (*betterStackTeamMembersResponse, error) {
+ endpoint, err := url.Parse(betterStackTeamMembersEndpoint)
+ if err != nil {
+ return nil, fmt.Errorf("cannot parse better stack team members URL: %w", err)
+ }
+
+ q := endpoint.Query()
+ q.Set("page", strconv.Itoa(page))
+
+ if d.teamName != "" {
+ q.Set("team_name", d.teamName)
+ }
+
+ endpoint.RawQuery = q.Encode()
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
+ if err != nil {
+ return nil, fmt.Errorf("cannot create better stack team members request: %w", err)
+ }
+
+ req.Header.Set("Accept", "application/json")
+
+ httpResp, err := d.httpClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("cannot execute better stack team members request: %w", err)
+ }
+
+ defer func() { _ = httpResp.Body.Close() }()
+
+ if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
+ return nil, fmt.Errorf("cannot fetch better stack team members: unexpected status %d", httpResp.StatusCode)
+ }
+
+ var resp betterStackTeamMembersResponse
+ if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil {
+ return nil, fmt.Errorf("cannot decode better stack team members response: %w", err)
+ }
+
+ return &resp, nil
+}
+
+// betterStackRole maps a Better Stack role token to a human-readable label.
+// Unknown roles (including the Enterprise "custom" roles) are passed through
+// unchanged so the reviewer still sees the source value.
+func betterStackRole(role string) string {
+ switch role {
+ case "admin":
+ return "Admin"
+ case "billing_admin":
+ return "Billing admin"
+ case "team_lead":
+ return "Team lead"
+ case "responder":
+ return "Responder"
+ case "member":
+ return "Member"
+ default:
+ return role
+ }
+}
+
+// betterStackIsAdmin flags roles with administrative control over team access:
+// admin (full control) and team_lead (can manage team members and roles).
+// billing_admin is billing-only, so it is not flagged.
+func betterStackIsAdmin(role string) bool {
+ return role == "admin" || role == "team_lead"
+}
+
+// betterStackActive maps the member record type to an explicit active signal:
+// confirmed members are active, pending invitations are not, and unknown
+// types leave the signal nil rather than fabricate one.
+func betterStackActive(memberType string) *bool {
+ switch memberType {
+ case "team_member":
+ return new(true)
+ case "team_member_invitation":
+ return new(false)
+ default:
+ return nil
+ }
+}
+
+func parseBetterStackTimestamp(value string) (time.Time, bool) {
+ if value == "" {
+ return time.Time{}, false
+ }
+
+ // time.Parse accepts a fractional second even when the layout omits it,
+ // so RFC3339 covers Better Stack's ".000Z" timestamps too.
+ t, err := time.Parse(time.RFC3339, value)
+ if err != nil {
+ return time.Time{}, false
+ }
+
+ return t, true
+}
diff --git a/pkg/accessreview/drivers/better_stack_test.go b/pkg/accessreview/drivers/better_stack_test.go
new file mode 100644
index 000000000..482379e63
--- /dev/null
+++ b/pkg/accessreview/drivers/better_stack_test.go
@@ -0,0 +1,135 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+package drivers
+
+import (
+ "context"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "go.probo.inc/probo/pkg/coredata"
+)
+
+func TestBetterStackDriver(t *testing.T) {
+ t.Parallel()
+
+ rec := newRecorder(t, "testdata/better_stack", "BETTER_STACK_TOKEN")
+
+ teamName := os.Getenv("BETTER_STACK_TEAM_NAME")
+ if teamName == "" {
+ teamName = "acme"
+ }
+
+ client := newVCRClient(rec, bearerAuth(os.Getenv("BETTER_STACK_TOKEN")))
+
+ records, err := NewBetterStackDriver(client, teamName).ListAccounts(context.Background())
+ require.NoError(t, err)
+ assert.Len(t, records, 1)
+
+ r := records[0]
+ assert.Equal(t, "alice@example.com", r.Email)
+ assert.Equal(t, "Alice Smith", r.FullName)
+ assert.Equal(t, "Admin", r.Role)
+ assert.True(t, r.IsAdmin)
+ assert.Equal(t, "101", r.ExternalID)
+ require.NotNil(t, r.Active)
+ assert.True(t, *r.Active)
+ require.NotNil(t, r.CreatedAt)
+ assert.Equal(t, coredata.MFAStatusUnknown, r.MFAStatus)
+}
+
+func TestBetterStackDriverPagination(t *testing.T) {
+ t.Parallel()
+
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, http.MethodGet, r.Method)
+ assert.Equal(t, "/api/v2/team-members", r.URL.Path)
+ assert.Equal(t, "acme", r.URL.Query().Get("team_name"))
+
+ w.Header().Set("Content-Type", "application/json")
+
+ if r.URL.Query().Get("page") == "2" {
+ _, _ = w.Write([]byte(`{"data":[{"id":"201","type":"team_member_invitation","attributes":{"email":"charlie@example.com","invited_at":"2023-10-28T12:00:00.000Z","role":"member"}}],"pagination":{"next":null}}`))
+
+ return
+ }
+
+ _, _ = w.Write([]byte(`{"data":[{"id":"101","type":"team_member","attributes":{"email":"alice@example.com","first_name":"Alice","last_name":"Smith","created_at":"2023-10-26T10:00:00.000Z","role":"admin"}},{"id":"102","type":"team_member","attributes":{"email":"bob@example.com","first_name":"Bob","last_name":"","created_at":"2023-10-27T11:00:00.000Z","role":"responder"}}],"pagination":{"next":"https://betterstack.com/api/v2/team-members?page=2&team_name=acme"}}`))
+ }))
+ defer srv.Close()
+
+ client := &http.Client{Transport: &hostRewriter{target: srv.URL}}
+
+ records, err := NewBetterStackDriver(client, "acme").ListAccounts(context.Background())
+ require.NoError(t, err)
+ require.Len(t, records, 3)
+
+ assert.Equal(t, "alice@example.com", records[0].Email)
+ assert.Equal(t, "Alice Smith", records[0].FullName)
+ assert.True(t, *records[0].Active)
+ assert.Equal(t, "bob@example.com", records[1].Email)
+ assert.Equal(t, "Bob", records[1].FullName)
+ assert.Equal(t, "charlie@example.com", records[2].Email)
+ require.NotNil(t, records[2].Active)
+ assert.False(t, *records[2].Active)
+}
+
+func TestBetterStackDriverListAccountsError(t *testing.T) {
+ t.Parallel()
+
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusUnauthorized)
+ _, _ = w.Write([]byte(`{"errors":"invalid token"}`))
+ }))
+ defer srv.Close()
+
+ client := &http.Client{Transport: &hostRewriter{target: srv.URL}}
+
+ _, err := NewBetterStackDriver(client, "acme").ListAccounts(context.Background())
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "unexpected status 401")
+}
+
+func TestBetterStackRole(t *testing.T) {
+ t.Parallel()
+
+ cases := []struct {
+ in string
+ want string
+ isAdmin bool
+ }{
+ {"admin", "Admin", true},
+ {"billing_admin", "Billing admin", false},
+ {"team_lead", "Team lead", true},
+ {"responder", "Responder", false},
+ {"member", "Member", false},
+ {"custom", "custom", false},
+ {"future_role", "future_role", false},
+ }
+
+ for _, c := range cases {
+ t.Run(c.in, func(t *testing.T) {
+ t.Parallel()
+
+ assert.Equal(t, c.want, betterStackRole(c.in))
+ assert.Equal(t, c.isAdmin, betterStackIsAdmin(c.in))
+ })
+ }
+}
diff --git a/pkg/accessreview/drivers/name_resolver.go b/pkg/accessreview/drivers/name_resolver.go
index 182f04c2d..df34050c4 100644
--- a/pkg/accessreview/drivers/name_resolver.go
+++ b/pkg/accessreview/drivers/name_resolver.go
@@ -706,6 +706,21 @@ func (r *resendNameResolver) ResolveInstanceName(_ context.Context) (string, err
return "Resend", nil
}
+// betterStackNameResolver returns the Better Stack team name captured when
+// the API-key connector was created. The team name is the human-readable
+// instance identifier, so no HTTP call is required.
+type betterStackNameResolver struct {
+ teamName string
+}
+
+func NewBetterStackNameResolver(teamName string) NameResolver {
+ return &betterStackNameResolver{teamName: teamName}
+}
+
+func (r *betterStackNameResolver) ResolveInstanceName(_ context.Context) (string, error) {
+ return r.teamName, nil
+}
+
// gitlabNameResolver resolves the GitLab group name.
type gitlabNameResolver struct {
httpClient *http.Client
diff --git a/pkg/accessreview/drivers/testdata/better_stack.yaml b/pkg/accessreview/drivers/testdata/better_stack.yaml
new file mode 100644
index 000000000..d4098d8d4
--- /dev/null
+++ b/pkg/accessreview/drivers/testdata/better_stack.yaml
@@ -0,0 +1,33 @@
+---
+version: 2
+interactions:
+ - id: 0
+ request:
+ proto: HTTP/1.1
+ proto_major: 1
+ proto_minor: 1
+ content_length: 0
+ host: betterstack.com
+ form:
+ page:
+ - "1"
+ team_name:
+ - acme
+ headers:
+ Accept:
+ - application/json
+ url: https://betterstack.com/api/v2/team-members?page=1&team_name=acme
+ method: GET
+ response:
+ proto: HTTP/2.0
+ proto_major: 2
+ proto_minor: 0
+ content_length: -1
+ uncompressed: true
+ body: '{"data":[{"id":"101","type":"team_member","attributes":{"email":"alice@example.com","first_name":"Alice","last_name":"Smith","created_at":"2026-06-09T13:24:53.712Z","role":"admin","mobile_app_platforms":[]}}],"pagination":{"first":"https://betterstack.com/api/v2/team-members?page=1&team_name=acme","last":"https://betterstack.com/api/v2/team-members?page=1&team_name=acme","prev":null,"next":null}}'
+ headers:
+ Content-Type:
+ - application/json; charset=utf-8
+ status: 200 OK
+ code: 200
+ duration: 100ms
diff --git a/pkg/connector/provider/better_stack.go b/pkg/connector/provider/better_stack.go
new file mode 100644
index 000000000..c246cd79e
--- /dev/null
+++ b/pkg/connector/provider/better_stack.go
@@ -0,0 +1,65 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+package provider
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "strings"
+
+ "go.gearno.de/kit/log"
+ "go.probo.inc/probo/pkg/accessreview/drivers"
+ "go.probo.inc/probo/pkg/coredata"
+)
+
+// betterStackRegistration wires the Better Stack Uptime access-review
+// connector. Better Stack has no third-party OAuth app for listing team
+// members (its OAuth is an end-user MCP sign-in), so the connector is
+// API-key only: the operator supplies a Bearer API token plus the team
+// name that scopes the team-members listing.
+func betterStackRegistration() *Registration {
+ return &Registration{
+ Provider: coredata.ConnectorProviderBetterStack,
+ DisplayName: "Better Stack",
+ SupportsAPIKey: true,
+ ProbeURL: "https://betterstack.com/api/v2/team-members",
+ ExtraSettings: []ExtraSetting{
+ {Key: "teamName", Label: "Team Name", Required: true},
+ },
+ NewDriver: func(_ context.Context, c *http.Client, conn *coredata.Connector, _ *log.Logger) (drivers.Driver, error) {
+ s, err := coredata.ConnectorSettings[coredata.BetterStackConnectorSettings](conn)
+ if err != nil {
+ return nil, fmt.Errorf("cannot read better stack connector settings: %w", err)
+ }
+
+ teamName := strings.TrimSpace(s.TeamName)
+ if teamName == "" {
+ return nil, fmt.Errorf("cannot create better stack driver: team_name is required")
+ }
+
+ return drivers.NewBetterStackDriver(c, teamName), nil
+ },
+ NewNameResolver: func(ctx context.Context, _ *http.Client, conn *coredata.Connector, logger *log.Logger) drivers.NameResolver {
+ s, err := coredata.ConnectorSettings[coredata.BetterStackConnectorSettings](conn)
+ if err != nil {
+ logger.ErrorCtx(ctx, "cannot read better stack connector settings", log.Error(err))
+ return nil
+ }
+
+ return drivers.NewBetterStackNameResolver(strings.TrimSpace(s.TeamName))
+ },
+ }
+}
diff --git a/pkg/connector/provider/builtin.go b/pkg/connector/provider/builtin.go
index 8a15c6856..7092c7919 100644
--- a/pkg/connector/provider/builtin.go
+++ b/pkg/connector/provider/builtin.go
@@ -24,6 +24,7 @@ func NewBuiltinRegistry() *Registry {
for _, reg := range []*Registration{
anthropicRegistration(),
asanaRegistration(),
+ betterStackRegistration(),
bitbucketRegistration(),
brexRegistration(),
clerkRegistration(),
diff --git a/pkg/coredata/connector_provider.go b/pkg/coredata/connector_provider.go
index 233108445..354e4c183 100644
--- a/pkg/coredata/connector_provider.go
+++ b/pkg/coredata/connector_provider.go
@@ -39,6 +39,7 @@ const (
ConnectorProviderSentry ConnectorProvider = "SENTRY"
ConnectorProviderSigNoz ConnectorProvider = "SIGNOZ"
ConnectorProviderSupabase ConnectorProvider = "SUPABASE"
+ ConnectorProviderBetterStack ConnectorProvider = "BETTER_STACK"
ConnectorProviderGitHub ConnectorProvider = "GITHUB"
ConnectorProviderIntercom ConnectorProvider = "INTERCOM"
ConnectorProviderResend ConnectorProvider = "RESEND"
@@ -87,6 +88,7 @@ func ConnectorProviders() []ConnectorProvider {
ConnectorProviderSentry,
ConnectorProviderSigNoz,
ConnectorProviderSupabase,
+ ConnectorProviderBetterStack,
ConnectorProviderGitHub,
ConnectorProviderIntercom,
ConnectorProviderResend,
@@ -131,6 +133,7 @@ func (v ConnectorProvider) IsValid() bool {
ConnectorProviderSentry,
ConnectorProviderSigNoz,
ConnectorProviderSupabase,
+ ConnectorProviderBetterStack,
ConnectorProviderGitHub,
ConnectorProviderIntercom,
ConnectorProviderResend,
diff --git a/pkg/coredata/connector_settings.go b/pkg/coredata/connector_settings.go
index dc2298840..3c2d34ab7 100644
--- a/pkg/coredata/connector_settings.go
+++ b/pkg/coredata/connector_settings.go
@@ -136,6 +136,10 @@ type (
ZendeskConnectorSettings struct {
Subdomain string `json:"subdomain"`
}
+
+ BetterStackConnectorSettings struct {
+ TeamName string `json:"team_name"`
+ }
)
// GrantType returns the OAuth2 grant type recorded on the connector's
diff --git a/pkg/coredata/migrations/20260608T417263Z.sql b/pkg/coredata/migrations/20260608T417263Z.sql
new file mode 100644
index 000000000..1bf9dd5b4
--- /dev/null
+++ b/pkg/coredata/migrations/20260608T417263Z.sql
@@ -0,0 +1,15 @@
+-- Copyright (c) 2026 Probo Inc .
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted, provided that the above
+-- copyright notice and this permission notice appear in all copies.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+-- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+-- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+-- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+-- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+-- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+-- PERFORMANCE OF THIS SOFTWARE.
+
+ALTER TYPE connector_provider ADD VALUE IF NOT EXISTS 'BETTER_STACK';
diff --git a/pkg/server/api/console/v1/connector_settings.go b/pkg/server/api/console/v1/connector_settings.go
index 686ec61f5..1da375490 100644
--- a/pkg/server/api/console/v1/connector_settings.go
+++ b/pkg/server/api/console/v1/connector_settings.go
@@ -154,6 +154,12 @@ func apiKeyConnectorSettings(input types.CreateAPIKeyConnectorInput) (json.RawMe
}
return json.Marshal(&coredata.OktaConnectorSettings{Domain: domain})
+ case coredata.ConnectorProviderBetterStack:
+ if input.BetterStackTeamName == nil || *input.BetterStackTeamName == "" {
+ return nil, fmt.Errorf("cannot create better stack connector: betterStackTeamName is required")
+ }
+
+ return json.Marshal(&coredata.BetterStackConnectorSettings{TeamName: *input.BetterStackTeamName})
}
return nil, nil
diff --git a/pkg/server/api/console/v1/graphql/connector.graphql b/pkg/server/api/console/v1/graphql/connector.graphql
index 4b574a57b..d255299db 100644
--- a/pkg/server/api/console/v1/graphql/connector.graphql
+++ b/pkg/server/api/console/v1/graphql/connector.graphql
@@ -28,6 +28,10 @@ enum ConnectorProvider
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderSigNoz")
SUPABASE
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderSupabase")
+ BETTER_STACK
+ @goEnum(
+ value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderBetterStack"
+ )
GITHUB @goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderGitHub")
INTERCOM
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderIntercom")
@@ -141,6 +145,7 @@ input CreateAPIKeyConnectorInput {
posthogRegion: String
posthogInstanceUrl: String
oktaDomain: String
+ betterStackTeamName: String
}
type CreateAPIKeyConnectorPayload {