Skip to content

Repository files navigation

Rownd SDK for Go

A comprehensive Go SDK for integrating Rownd authentication, user management, and group management into your applications.

Installation

go get github.com/rownd/client-go/pkg/rownd

Features

  • Token validation and management with EdDSA support
  • User authentication and profile management
  • Group management with member roles and invites
  • HTTP middleware for authentication
  • Comprehensive error handling
  • Configurable caching for JWKS and WKC

Quick Start

package main
import (
"context""log""github.com/rownd/client-go/pkg/rownd"
)
funcmain() {
// Initialize client with optionsclient, err:=rownd.NewClient(
rownd.WithAppKey("YOUR_APP_KEY"),
rownd.WithAppSecret("YOUR_APP_SECRET"),
rownd.WithBaseURL("https://api.rownd.io"),
)
iferr!=nil {
log.Fatal(err)
}
ctx:=context.Background()
// Create or update a useruser, err:=client.Users.CreateOrUpdate(ctx, rownd.CreateOrUpdateUserRequest{
Data: map[string]interface{}{
"email": "user@example.com",
"first_name": "John",
"last_name": "Doe",
},
})
iferr!=nil {
log.Fatal(err)
}
log.Printf("User ID: %s", user.GetID())
}

Quick Start with Examples

Creating Users

// Let Rownd generate a UUIDuser, err:=client.Users.CreateOrUpdate(ctx, rownd.CreateOrUpdateUserRequest{
UserID: "__default__", // Special value that tells Rownd to generate a user ID. Can be `__rowndid__`, `__uuid__`, `__objectid__`, or `__default__` for your app's configured default behavior.Data: map[string]interface{}{
"email": "user@example.com",
"first_name": "John",
"last_name": "Doe",
},
})
// Response:// user = {// ID: "user_a7b53gwdaml5jt7t71442nt7",// State: "enabled",// AuthLevel: "unverified",// Data: {// "email": "user@example.com",// "first_name": "John",// "last_name": "Doe",// "user_id": "user_a7b53gwdaml5jt7t71442nt7"// }// }// Use your own IDuser, err:=client.Users.CreateOrUpdate(ctx, rownd.CreateOrUpdateUserRequest{
UserID: "custom_id_12345",
Data: map[string]interface{}{
"email": "user@example.com",
},
})

Looking Up Users

// Lookup by emailusers, err:=client.Users.List(ctx, rownd.ListUsersRequest{
Fields: []string{"email", "first_name", "last_name", "user_id"}, // Specify fields to returnLookupFilter: []string{"user@example.com"},
})
// Response:// users = {// TotalResults: 1,// Results: [{// ID: "user_a7b53gwdaml5jt7t71442nt7",// State: "enabled",// AuthLevel: "verified",// Data: {// "email": "user@example.com",// "first_name": "John",// "last_name": "Doe"// },// VerifiedData: {// "email": "user@example.com"// }// }]// }// Pagination exampleusers, err:=client.Users.List(ctx, rownd.ListUsersRequest{
PageSize: ToPtr(10), // Get 10 results per pageAfter: ToPtr("user_lastid"), // Start after this user ID
})

Group Management Examples

// Create a groupgroup, err:=client.Groups.Create(ctx, rownd.CreateGroupRequest{
Name: "Engineering Team",
AdmissionPolicy: rownd.AdmissionPolicyInviteOnly,
Meta: map[string]any{
"department": "Engineering",
"cost_center": "ENG-123",
},
})
// Response:// group = {// ID: "group_a3l1n2lsnb3q0xbul9enjnh7",// Name: "Engineering Team",// AdmissionPolicy: "invite_only",// Meta: {// "department": "Engineering",// "cost_center": "ENG-123"// },// CreatedAt: "2024-03-01T12:00:00Z",// UpdatedAt: "2024-03-01T12:00:00Z"// }// Create an inviteinvite, err:=client.GroupInvites.Create(ctx, rownd.CreateGroupInviteRequest{
GroupID: group.ID,
Email: "new@example.com",
Roles: []string{"member"},
RedirectURL: "/welcome",
})
// Response:// invite = {// Link: "https://app.rownd.io/invite/abc123...",// Invitation: {// ID: "invite_xyz789",// GroupID: "group_a3l1n2lsnb3q0xbul9enjnh7",// Email: "new@example.com",// Roles: ["member"],// State: "pending",// CreatedAt: "2024-03-01T12:01:00Z"// }// }

Token Validation with Claims

token, err:=client.ValidateToken(ctx, "your-jwt-token")
// Response:// token = {// UserID: "user_a7b53gwdaml5jt7t71442nt7",// AccessToken: "original-jwt-token",// Claims: {// Sub: "user_a7b53gwdaml5jt7t71442nt7",// Iss: "https://api.rownd.io",// Aud: ["app:app_xyz123"],// Exp: 1709312400,// Iat: 1709308800,// AppUserID: "user_a7b53gwdaml5jt7t71442nt7",// IsUserVerified: true,// IsAnonymous: false,// AuthLevel: "verified"// }// }

Helpful Utilities

// Convert values to pointers (useful for optional fields)pageSize:=rownd.ToPtr(10)
after:=rownd.ToPtr("some_id")
// Get value from pointer with fallbackvalue:=rownd.ToValue(optionalPtr) // Returns actual value or zero value if nil// Extract token from context (in HTTP handlers)token:=rownd.TokenFromCtx(r.Context())
iftoken!=nil {
userID:=token.UserIDauthLevel:=token.Claims.AuthLevel
}

Authentication & Token Validation

Token Validation

// Validate a tokentoken, err:=client.ValidateToken(ctx, "your-jwt-token")
iferr!=nil {
log.Fatal(err)
}
// Access token claimslog.Printf("User ID: %s", token.UserID)
log.Printf("Auth Level: %s", token.Claims.AuthLevel)

HTTP Middleware

import"github.com/rownd/client-go/pkg/rownd/middleware"// Create middleware handlerhandler, err:=rowndmiddleware.NewHandler(client, rowndmiddleware.WithErrorHandler(func(w http.ResponseWriter, r*http.Request, errerror) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
}),
)
// Use middlewarerouter.Use(rowndmiddleware.WithAuthentication(handler))

User Management

User Operations

// Get useruser, err:=client.Users.Get(ctx, rownd.GetUserRequest{
UserID: "user_id",
})
// List/lookup usersusers, err:=client.Users.List(ctx, rownd.ListUsersRequest{
Fields: []string{"email", "first_name", "last_name"},
LookupFilter: []string{"user@example.com"},
})
// Delete usererr:=client.Users.Delete(ctx, rownd.DeleteUserRequest{
UserID: "user_id",
})

User Attributes

User attributes are key-value pairs where values are arrays of strings. They can be used to store additional metadata like app variants, subscription status, or custom application data.

// Create user with attributesuser, err:=client.Users.CreateOrUpdate(ctx, rownd.CreateOrUpdateUserRequest{
UserID: "__UUID__",
Data: map[string]interface{}{
"email": "user@example.com",
"first_name": "John",
},
Attributes: map[string][]string{
"rownd:app_variants": {"variant_1", "variant_2"},
"myapp:subscription_status": {"active"},
"myapp:loyalty_points": {"100"},
},
})
// Get user attributesattrs:=user.GetAttributes()
// attrs = {// "rownd:app_variants": ["variant_1", "variant_2"],// "myapp:subscription_status": ["active"],// "myapp:loyalty_points": ["100"]// }// Add or update attributes (using PATCH)user, err=client.Users.AddAttributes(ctx, rownd.AddAttributesRequest{
UserID: "user_id",
Attributes: map[string][]string{
"myapp:loyalty_points": {"200"}, // Updates existing"myapp:tier": {"gold"}, // Adds new
},
})
// Delete attributes (requires GET + PUT)user, err=client.Users.DeleteAttributes(ctx, rownd.DeleteAttributesRequest{
UserID: "user_id",
AttributeKeys: []string{"myapp:subscription_status", "myapp:tier"},
})
// You can also manage attributes directly via Patch/CreateOrUpdateuser, err=client.Users.Patch(ctx, rownd.PatchUserRequest{
UserID: "user_id",
Data: map[string]any{
"email": "newemail@example.com",
},
Attributes: map[string][]string{
"myapp:new_attribute": {"value1", "value2"},
},
})

Group Management

Groups

// Create groupgroup, err:=client.Groups.Create(ctx, rownd.CreateGroupRequest{
Name: "Engineering Team",
AdmissionPolicy: rownd.AdmissionPolicyInviteOnly,
Meta: map[string]any{
"department": "Engineering",
},
})
// List groupsgroups, err:=client.Groups.List(ctx, rownd.ListGroupsRequest{})
// Delete grouperr:=client.Groups.Delete(ctx, rownd.DeleteGroupRequest{
GroupID: "group_id",
})

Group Invites

// Create inviteinvite, err:=client.GroupInvites.Create(ctx, rownd.CreateGroupInviteRequest{
GroupID: "group_id",
Email: "new@example.com",
Roles: []string{"member"},
RedirectURL: "/welcome",
})
// List invitesinvites, err:=client.GroupInvites.List(ctx, rownd.ListGroupInvitesRequest{
GroupID: "group_id",
})
// Delete inviteerr:=client.GroupInvites.Delete(ctx, rownd.DeleteGroupInviteRequest{
GroupID: "group_id",
InviteID: "invite_id",
})

Group Membership Management

Understanding Member ID vs User ID

In Rownd's group system, there are two important identifiers:

  • user_id: The unique identifier for a Rownd user (e.g., "user_a7b53gwdaml5jt7t71442ng7")
  • member_id: The unique identifier for a user's membership in a specific group (e.g., "member_dnn5g4e3q5aptail2gr43kpj")

A single user can be a member of multiple groups, with a different member_id for each group membership.

// Example group member structuretypeGroupMemberstruct {
IDstring`json:"id"`// This is the member_idUserIDstring`json:"user_id"`// This is the user_idRoles []string`json:"roles"`Statestring`json:"state"`Profilemap[string]interface{} `json:"profile"`GroupIDstring`json:"group_id"`
}

Managing Group Members

// Add a user to a groupmember, err:=client.GroupMembers.Create(ctx, rownd.CreateGroupMemberRequest{
GroupID: "group_a3l1n2lsnb3q0xbul9enjnh7",
UserID: "user_a7b53gwdaml5jt7t71442nt7",
Roles: []string{"editor", "viewer"},
})
// Response:// member = {// ID: "member_dnn5g4e3q6aptail2gr43kpj", // The member_id// UserID: "user_a7b53gwdaml5jt7t71442nt7", // The user_id// Roles: ["editor", "viewer"],// State: "active",// Profile: {// "email": "user@example.com",// "first_name": "John"// },// GroupID: "group_a3l1n2lsnb3q0xbul9enjnh7"// }// Update a member's roles using member_idupdatedMember, err:=client.GroupMembers.Update(ctx, rownd.UpdateGroupMemberRequest{
GroupID: "group_a3l1n2lsnb3q0xbul9enjnh7",
MemberID: "member_dnn5g4e3q6aptail2gr43kpj", // Use member_id, not user_idRoles: []string{"admin"},
})
// List group membersmembers, err:=client.GroupMembers.List(ctx, rownd.ListGroupMembersRequest{
GroupID: "group_a3l1n2lsnb3q0xbul9enjnh7",
})
// Response:// members = {// TotalResults: 2,// Results: [{// ID: "member_dnn5g4e3q6aptail2gr43kpj",// UserID: "user_a7b53gwdaml5jt7t71442nt7",// Roles: ["admin"],// State: "active",// Profile: {// "email": "user@example.com"// }// }, {// ID: "member_kll8h7g2p9qbxyzw4m5njth8",// UserID: "user_b8c64hwdaml5kt8u82553ou8",// Roles: ["viewer"],// State: "active",// Profile: {// "email": "another@example.com"// }// }]// }// Remove a member from a group using member_iderr:=client.GroupMembers.Delete(ctx, rownd.DeleteGroupMemberRequest{
GroupID: "group_a3l1n2lsnb3q0xbul9enjnh7",
MemberID: "member_dnn5g4e3q6aptail2gr43kpj", // Use member_id, not user_id
})

Important Notes About Group Membership

  1. Member ID vs User ID

    • Use member_id when managing a specific membership (updating roles, removing from group)
    • Use user_id when adding a new member to a group
    • A user (user_id) can have multiple memberships (member_ids) across different groups
  2. Group Ownership

    • Groups must always have at least one owner
    • When removing the last owner, transfer ownership first
    • Example of transferring ownership:
    // Transfer ownership before removing the last owner_, err=client.GroupMembers.Update(ctx, rownd.UpdateGroupMemberRequest{
    GroupID: "group_id",
    MemberID: "new_owner_member_id",
    Roles: []string{"owner", "member"},
    })
  3. Member States

    • active: Normal membership
    • suspended: Temporarily restricted access
    • invited: Pending acceptance of invitation
  4. Common Role Types

    • owner: Full administrative control
    • admin: Can manage members and content
    • editor: Can modify content
    • viewer: Read-only access
    • Custom roles can be defined as needed

Group Ownership and Member Management Rules

Group Ownership Rules

  1. Automatic Owner Assignment

    • The first member added to a group automatically receives the "owner" role
    • Example of first member creation:
    // First member automatically becomes ownermember, err:=client.GroupMembers.Create(ctx, rownd.CreateGroupMemberRequest{
    GroupID: "group_id",
    UserID: "user_id",
    Roles: []string{"member"}, // "owner" will be automatically added
    })
    // Response:// member = {// ID: "member_abc123",// UserID: "user_id",// Roles: ["owner", "member"], // Note: "owner" was automatically added// State: "active"// }
  2. Owner Requirements

    • Every group must maintain at least one owner at all times
    • Attempting to remove the last owner will result in an error
    // This will fail if it's the last ownererr:=client.GroupMembers.Delete(ctx, rownd.DeleteGroupMemberRequest{
    GroupID: "group_id",
    MemberID: "last_owner_member_id", // Will return error if last owner
    })
  3. Group Deletion Requirements

    • A group must be deleted before removing its last member
    • Correct order of operations:
    // Correct order: Delete group first, which removes all memberserr:=client.Groups.Delete(ctx, rownd.DeleteGroupRequest{
    GroupID: "group_id",
    })
    // Incorrect: Will fail if trying to remove last member while group existserr:=client.GroupMembers.Delete(ctx, rownd.DeleteGroupMemberRequest{
    GroupID: "group_id",
    MemberID: "last_member_id", // Will return error
    })

Best Practices for Owner Management

  1. Transferring Ownership

    // First, add owner role to another member_, err=client.GroupMembers.Update(ctx, rownd.UpdateGroupMemberRequest{
    GroupID: "group_id",
    MemberID: "new_owner_member_id",
    Roles: []string{"owner", "member"},
    })
    iferr!=nil {
    returnerr
    }
    // Then, you can safely remove owner role from the previous owner_, err=client.GroupMembers.Update(ctx, rownd.UpdateGroupMemberRequest{
    GroupID: "group_id",
    MemberID: "old_owner_member_id",
    Roles: []string{"member"},
    })
  2. Checking Owner Status

    members, err:=client.GroupMembers.List(ctx, rownd.ListGroupMembersRequest{
    GroupID: "group_id",
    })
    // Count ownersownerCount:=0for_, member:=rangemembers.Results {
    for_, role:=rangemember.Roles {
    ifrole=="owner" {
    ownerCount++break
    }
    }
    }
    // Ensure there's at least one ownerifownerCount==0 {
    log.Fatal("Group must have at least one owner")
    }
  3. Group Cleanup Process

    // Proper group cleanup sequencefunccleanupGroup(ctx context.Context, client*rownd.Client, groupIDstring) error {
    // 1. First, delete the group (this will remove all members)err:=client.Groups.Delete(ctx, rownd.DeleteGroupRequest{
    GroupID: groupID,
    })
    iferr!=nil {
    returnfmt.Errorf("failed to delete group: %w", err)
    }
    // No need to manually delete members - they are automatically removedreturnnil
    }

Error Handling

The SDK provides structured error types for better error handling:

iferr!=nil {
switche:=err.(type) {
case*rownd.Error:
switche.Kind {
caserownd.ErrAuthentication:
log.Printf("Authentication error: %v", e)
caserownd.ErrValidation:
log.Printf("Validation error: %v", e)
caserownd.ErrAPI:
log.Printf("API error: %v", e)
caserownd.ErrNetwork:
log.Printf("Network error: %v", e)
caserownd.ErrNotFound:
log.Printf("Not found error: %v", e)
}
case*rownd.MultiError:
log.Printf("Multiple errors occurred: %v", e)
default:
log.Printf("Unknown error: %v", err)
}
}

Configuration Options

Client Options

client, err:=rownd.NewClient(
rownd.WithAppKey("key"),
rownd.WithAppSecret("secret"),
rownd.WithBaseURL("https://api.rownd.io"),
rownd.WithWKCCacheDuration(time.Hour),
rownd.WithJWKsCacheDuration(time.Hour),
)

Request Options

client.Users.Get(ctx, request, rownd.RequestWithHeader("X-Custom-Header", "value"),
)

Testing

Run all tests:

go test ./...

Run specific tests:

go test -v ./... -run TestRowndUsers

Run with timeout:

go test -v ./... -timeout 30s

Types Reference

Auth Levels

const (
AuthLevelInstantAuthLevel="instant"AuthLevelUnverifiedAuthLevel="unverified"AuthLevelGuestAuthLevel="guest"AuthLevelVerifiedAuthLevel="verified"
)

Group Admission Policies

const (
AdmissionPolicyInviteOnlyAdmissionPolicy="invite_only"AdmissionPolicyOpenAdmissionPolicy="open"
)

License

This project is licensed under the MIT License - see the LICENSE file for details.

Environment Setup

Using Environment Variables

Create a .env file in your project root:

# .envROWND_APP_KEY=key_bd81v4usfn4c9wh6i83c13akROWND_APP_SECRET=ras_32769e81.0.002bc537079f78d4bc890214fd85c63b313c0ROWND_APP_ID=app_xkbuml48qs3tyxxjjpaxeemvROWND_BASE_URL=https://api.rownd.io

Load environment variables in your code:

package main
import (
"github.com/joho/godotenv""github.com/rownd/client-go/pkg/rownd""log""os"
)
funcmain() {
// Load .env fileiferr:=godotenv.Load(); err!=nil {
log.Printf("Warning: .env file not found")
}
// Initialize client with environment variablesclient, err:=rownd.NewClient(
rownd.WithAppKey(os.Getenv("ROWND_APP_KEY")),
rownd.WithAppSecret(os.Getenv("ROWND_APP_SECRET")),
rownd.WithBaseURL(os.Getenv("ROWND_BASE_URL")),
)
iferr!=nil {
log.Fatal(err)
}
}

Environment Files

  1. Add .env to your .gitignore:
# .gitignore.env
  1. For testing, create a separate .env.test:
# .env.testROWND_TEST_APP_KEY=test_key_hereROWND_TEST_APP_SECRET=test_secret_hereROWND_TEST_APP_ID=test_app_id_hereROWND_TEST_BASE_URL=https://api.rownd.io
  1. Load different env files based on environment:
funcloadEnv() {
env:=os.Getenv("GO_ENV")
ifenv=="test" {
godotenv.Load(".env.test")
}
}

About

Rownd client SDK for Go

Resources

Stars

0 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages