Skip to Content
IntegrationsBuilding Custom

Building Custom Integrations

This guide walks through creating a custom Creddy integration from scratch.

Overview

Creddy integrations are implemented as plugins — standalone binaries that communicate with Creddy core via gRPC. The Plugin SDK  handles the communication layer — you just implement the credential logic.

Prerequisites

  • Go 1.21 or later
  • Basic understanding of the service you’re integrating

Quick Start

1. Create the project

mkdir creddy-myservice cd creddy-myservice go mod init github.com/yourorg/creddy-myservice

2. Add the SDK dependency

go get github.com/getcreddy/creddy-plugin-sdk

3. Implement the plugin

Create main.go:

package main import ( sdk "github.com/getcreddy/creddy-plugin-sdk" ) func main() { sdk.ServeWithStandalone(&MyServicePlugin{}, nil) }

Create plugin.go:

package main import ( "context" "encoding/json" "fmt" "time" sdk "github.com/getcreddy/creddy-plugin-sdk" ) type MyServicePlugin struct { config *Config } type Config struct { APIKey string `json:"api_key"` BaseURL string `json:"base_url"` } // Info returns plugin metadata func (p *MyServicePlugin) Info(ctx context.Context) (*sdk.PluginInfo, error) { return &sdk.PluginInfo{ Name: "myservice", Version: "0.1.0", Description: "MyService API tokens", MinCreddyVersion: "0.4.0", }, nil } // Scopes returns the scope patterns this plugin handles func (p *MyServicePlugin) Scopes(ctx context.Context) ([]sdk.ScopeSpec, error) { return []sdk.ScopeSpec{ { Pattern: "myservice:*", Description: "Full access to MyService", Examples: []string{"myservice:*", "myservice:read"}, }, }, nil } // Configure parses and validates the configuration func (p *MyServicePlugin) Configure(ctx context.Context, configJSON string) error { var config Config if err := json.Unmarshal([]byte(configJSON), &config); err != nil { return fmt.Errorf("invalid config: %w", err) } if config.APIKey == "" { return fmt.Errorf("api_key is required") } p.config = &config return nil } // Validate tests connectivity with the configured credentials func (p *MyServicePlugin) Validate(ctx context.Context) error { if p.config == nil { return fmt.Errorf("plugin not configured") } // TODO: Make a test API call to verify credentials // Example: _, err := p.client.GetCurrentUser(ctx) return nil } // GetCredential generates a new ephemeral credential func (p *MyServicePlugin) GetCredential(ctx context.Context, req *sdk.CredentialRequest) (*sdk.Credential, error) { if p.config == nil { return nil, fmt.Errorf("plugin not configured") } // TODO: Call your service's API to generate a token // This is where the actual integration logic goes token := "generated-token-here" expiresAt := time.Now().Add(req.TTL) return &sdk.Credential{ Value: token, ExpiresAt: expiresAt, ExternalID: "optional-id-for-revocation", Metadata: map[string]string{ "scope": req.Scope, }, }, nil } // RevokeCredential revokes a previously issued credential func (p *MyServicePlugin) RevokeCredential(ctx context.Context, externalID string) error { // TODO: Call your service's API to revoke the token // Return nil if revocation isn't supported return nil } // MatchScope checks if this plugin handles the given scope func (p *MyServicePlugin) MatchScope(ctx context.Context, scope string) (bool, error) { // Return true if scope starts with "myservice:" return len(scope) > 10 && scope[:10] == "myservice:", nil }

4. Build and test

# Build go build -o creddy-myservice . # Test standalone ./creddy-myservice info ./creddy-myservice scopes # Test with config echo '{"api_key": "test-key"}' > test-config.json ./creddy-myservice validate --config test-config.json ./creddy-myservice get --config test-config.json --scope "myservice:test" --ttl 10m

5. Install locally

cp creddy-myservice ~/.creddy/plugins/

Plugin Interface

The SDK defines this interface:

type Plugin interface { // Metadata Info(ctx context.Context) (*PluginInfo, error) Scopes(ctx context.Context) ([]ScopeSpec, error) // Configuration Configure(ctx context.Context, config string) error Validate(ctx context.Context) error // Credentials GetCredential(ctx context.Context, req *CredentialRequest) (*Credential, error) RevokeCredential(ctx context.Context, externalID string) error MatchScope(ctx context.Context, scope string) (bool, error) }

PluginInfo

type PluginInfo struct { Name string // Plugin identifier (e.g., "github") Version string // Semantic version (e.g., "1.2.3") Description string // Human-readable description MinCreddyVersion string // Minimum Creddy version required }

ScopeSpec

type ScopeSpec struct { Pattern string // Scope pattern (e.g., "myservice:*") Description string // What this scope grants Examples []string // Example scope values }

CredentialRequest

type CredentialRequest struct { Agent Agent // Agent requesting the credential Scope string // Requested scope TTL time.Duration // Requested time-to-live Parameters map[string]string // Additional parameters } type Agent struct { ID string // Unique agent identifier Name string // Human-readable name Scopes []string // Agent's authorized scopes }

Credential

type Credential struct { Value string // The credential value (token, key, etc.) ExpiresAt time.Time // When the credential expires ExternalID string // Optional ID for revocation Metadata map[string]string // Optional additional data }

Development Workflow

Standalone Mode

The SDK provides a CLI for testing without Creddy:

./creddy-myservice info # Show plugin metadata ./creddy-myservice scopes # List supported scopes ./creddy-myservice validate --config X # Validate configuration ./creddy-myservice get --config X --scope Y --ttl 10m ./creddy-myservice revoke --config X --external-id Z

Local Development

Use CREDDY_PLUGIN_DIR to point Creddy at your development build:

# Terminal 1: Build and watch go build -o ./bin/creddy-myservice . && \ CREDDY_PLUGIN_DIR=./bin creddy server # Terminal 2: Test creddy get myservice --scope "myservice:test"

Debug Logging

Enable verbose plugin communication:

CREDDY_PLUGIN_DEBUG=1 creddy get myservice --scope "myservice:test"

Best Practices

Error Handling

Return clear, actionable errors:

// Bad return nil, fmt.Errorf("failed") // Good return nil, fmt.Errorf("myservice API error: %s (status %d)", resp.Message, resp.StatusCode)

Configuration Validation

Validate early in Configure():

func (p *MyPlugin) Configure(ctx context.Context, configJSON string) error { var config Config if err := json.Unmarshal([]byte(configJSON), &config); err != nil { return fmt.Errorf("invalid JSON: %w", err) } // Validate required fields if config.APIKey == "" { return fmt.Errorf("api_key is required") } // Validate format if !strings.HasPrefix(config.APIKey, "sk_") { return fmt.Errorf("api_key must start with 'sk_'") } p.config = &config return nil }

Scope Matching

Be precise with scope matching:

func (p *MyPlugin) MatchScope(ctx context.Context, scope string) (bool, error) { // Only match scopes that start with your prefix if !strings.HasPrefix(scope, "myservice:") { return false, nil } // Validate the scope is well-formed parts := strings.Split(scope, ":") if len(parts) < 2 { return false, nil } return true, nil }

Credential TTL

Respect the requested TTL when possible:

func (p *MyPlugin) GetCredential(ctx context.Context, req *sdk.CredentialRequest) (*sdk.Credential, error) { // Use requested TTL, but cap at service maximum ttl := req.TTL if ttl > 24*time.Hour { ttl = 24 * time.Hour // Service max } // ... generate credential with this TTL }

Testing

Unit Tests

func TestPlugin_Configure(t *testing.T) { p := &MyServicePlugin{} // Valid config err := p.Configure(context.Background(), `{"api_key": "sk_test"}`) if err != nil { t.Fatalf("expected no error, got: %v", err) } // Missing required field err = p.Configure(context.Background(), `{}`) if err == nil { t.Fatal("expected error for missing api_key") } }

Integration Tests

func TestPlugin_GetCredential(t *testing.T) { if os.Getenv("MYSERVICE_API_KEY") == "" { t.Skip("MYSERVICE_API_KEY not set") } p := &MyServicePlugin{} config := fmt.Sprintf(`{"api_key": "%s"}`, os.Getenv("MYSERVICE_API_KEY")) if err := p.Configure(context.Background(), config); err != nil { t.Fatalf("configure failed: %v", err) } cred, err := p.GetCredential(context.Background(), &sdk.CredentialRequest{ Scope: "myservice:test", TTL: 10 * time.Minute, }) if err != nil { t.Fatalf("get credential failed: %v", err) } if cred.Value == "" { t.Fatal("expected non-empty credential value") } }

Distribution

Building Releases

# Build for multiple platforms GOOS=darwin GOARCH=arm64 go build -o dist/creddy-myservice-darwin-arm64 . GOOS=darwin GOARCH=amd64 go build -o dist/creddy-myservice-darwin-amd64 . GOOS=linux GOARCH=amd64 go build -o dist/creddy-myservice-linux-amd64 . GOOS=linux GOARCH=arm64 go build -o dist/creddy-myservice-linux-arm64 .

GitHub Releases

Create a release with binaries attached. Users can install directly:

creddy plugin install https://github.com/yourorg/creddy-myservice/releases/download/v0.1.0/creddy-myservice-linux-amd64.tar.gz

Plugin Registry

For wider distribution, consider submitting to the community plugins list in Third-Party Plugins.

Examples


Questions? Open an issue  or check the Plugin SDK  repository.

Last updated on

Apache 2.0 2026 © Creddy