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"
)
func main() {
// Initialize client with options
client, err := rownd.NewClient(
rownd.WithAppKey("YOUR_APP_KEY"),
rownd.WithAppSecret("YOUR_APP_SECRET"),
rownd.WithBaseURL("https://api.rownd.io"),
)
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
// Create or update a user
user, err := client.Users.CreateOrUpdate(ctx, rownd.CreateOrUpdateUserRequest{
Data: map[string]interface{}{
"email": "user@example.com",
"first_name": "John",
"last_name": "Doe",
},
})
if err != nil {
log.Fatal(err)
}
log.Printf("User ID: %s", user.GetID())
}
Quick start with examples
Creating users
// Let Rownd generate an ID
user, 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 ID
user, err := client.Users.CreateOrUpdate(ctx, rownd.CreateOrUpdateUserRequest{
UserID: "custom_id_12345",
Data: map[string]interface{}{
"email": "user@example.com",
},
})
Searching for users
// Lookup by email
users, err := client.Users.List(ctx, rownd.ListUsersRequest{
Fields: []string{"email", "first_name", "last_name", "user_id"}, // Specify fields to return
LookupFilter: []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 example
users, err := client.Users.List(ctx, rownd.ListUsersRequest{
PageSize: ToPtr(10), // Get 10 results per page
After: ToPtr("user_lastid"), // Start after this user ID
})
Group management examples
// Create a group
group, 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 invite
invite, 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 fallback
value := rownd.ToValue(optionalPtr) // Returns actual value or zero value if nil
// Extract token from context (in HTTP handlers)
token := rownd.TokenFromCtx(r.Context())
if token != nil {
userID := token.UserID
authLevel := token.Claims.AuthLevel
}
Authentication & token validation
Token validation
// Validate a token
token, err := client.ValidateToken(ctx, "your-jwt-token")
if err != nil {
log.Fatal(err)
}
// Access token claims
log.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 handler
handler, err := rowndmiddleware.NewHandler(client,
rowndmiddleware.WithErrorHandler(func(w http.ResponseWriter, r *http.Request, err error) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
}),
)
// Use middleware
router.Use(rowndmiddleware.WithAuthentication(handler))
User management
User operations (CRUD)
// Get user
user, err := client.Users.Get(ctx, rownd.GetUserRequest{
UserID: "user_id",
})
// List/lookup users
users, err := client.Users.List(ctx, rownd.ListUsersRequest{
Fields: []string{"email", "first_name", "last_name"},
LookupFilter: []string{"user@example.com"},
})
// Delete user
err := client.Users.Delete(ctx, rownd.DeleteUserRequest{
UserID: "user_id",
})
Group management
Groups
// Create group
group, err := client.Groups.Create(ctx, rownd.CreateGroupRequest{
Name: "Engineering Team",
AdmissionPolicy: rownd.AdmissionPolicyInviteOnly,
Meta: map[string]any{
"department": "Engineering",
},
})
// List groups
groups, err := client.Groups.List(ctx, rownd.ListGroupsRequest{})
// Delete group
err := client.Groups.Delete(ctx, rownd.DeleteGroupRequest{
GroupID: "group_id",
})
Group invites
// Create invite
invite, err := client.GroupInvites.Create(ctx, rownd.CreateGroupInviteRequest{
GroupID: "group_id",
Email: "new@example.com",
Roles: []string{"member"},
RedirectURL: "/welcome",
})
// List invites
invites, err := client.GroupInvites.List(ctx, rownd.ListGroupInvitesRequest{
GroupID: "group_id",
})
// Delete invite
err := client.GroupInvites.Delete(ctx, rownd.DeleteGroupInviteRequest{
GroupID: "group_id",
InviteID: "invite_id",
})
Group membership management
The difference between Group IDs and User IDs
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 structure
type GroupMember struct {
ID string `json:"id"` // This is the member_id
UserID string `json:"user_id"` // This is the user_id
Roles []string `json:"roles"`
State string `json:"state"`
Profile map[string]interface{} `json:"profile"`
GroupID string `json:"group_id"`
}
Managing group members
// Add a user to a group
member, 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_id
updatedMember, err := client.GroupMembers.Update(ctx, rownd.UpdateGroupMemberRequest{
GroupID: "group_a3l1n2lsnb3q0xbul9enjnh7",
MemberID: "member_dnn5g4e3q6aptail2gr43kpj", // Use member_id, not user_id
Roles: []string{"admin"},
})
// List group members
members, 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_id
err := client.GroupMembers.Delete(ctx, rownd.DeleteGroupMemberRequest{
GroupID: "group_a3l1n2lsnb3q0xbul9enjnh7",
MemberID: "member_dnn5g4e3q6aptail2gr43kpj", // Use member_id, not user_id
})
Important notes about group membership
-
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_id
s) across different groups
-
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"},
})
-
Member States
active
: Normal membership
suspended
: Temporarily restricted access
invited
: Pending acceptance of invitation
-
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
-
Automatic owner assignment
- The first member added to a group automatically receives the “owner” role
- Example of first member creation:
// First member automatically becomes owner
member, 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"
// }
-
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 owner
err := client.GroupMembers.Delete(ctx, rownd.DeleteGroupMemberRequest{
GroupID: "group_id",
MemberID: "last_owner_member_id", // Will return error if last owner
})
-
Group deletion requirements
- A group must have at least one member. To remove all members, delete the group.
- Correct order of operations:
// Correct order: Delete group first, which removes all members
err := client.Groups.Delete(ctx, rownd.DeleteGroupRequest{
GroupID: "group_id",
})
// Incorrect: Will fail if trying to remove last member while group exists
err := client.GroupMembers.Delete(ctx, rownd.DeleteGroupMemberRequest{
GroupID: "group_id",
MemberID: "last_member_id", // Will return error
})
Error handling
The SDK provides structured error types for better error handling:
if err != nil {
switch e := err.(type) {
case *rownd.Error:
switch e.Kind {
case rownd.ErrAuthentication:
log.Printf("Authentication error: %v", e)
case rownd.ErrValidation:
log.Printf("Validation error: %v", e)
case rownd.ErrAPI:
log.Printf("API error: %v", e)
case rownd.ErrNetwork:
log.Printf("Network error: %v", e)
case rownd.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:
Run specific tests:
go test -v ./... -run TestRowndUsers
Run with timeout:
go test -v ./... -timeout 30s
Types reference
Auth levels
const (
AuthLevelInstant AuthLevel = "instant"
AuthLevelUnverified AuthLevel = "unverified"
AuthLevelGuest AuthLevel = "guest"
AuthLevelVerified AuthLevel = "verified"
)
Group admission policies
const (
AdmissionPolicyInviteOnly AdmissionPolicy = "invite_only"
AdmissionPolicyOpen AdmissionPolicy = "open"
)
Environment setup
Using environment variables
Create a .env
file in your project root:
# .env
ROWND_APP_KEY=key_bd81v4usfn4c9wh6i83c13ak
ROWND_APP_SECRET=ras_32769e81.0.002bc537079f78d4bc890214fd85c63b313c0
ROWND_APP_ID=app_xkbuml48qs3tyxxjjpaxeemv
ROWND_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"
)
func main() {
// Load .env file
if err := godotenv.Load(); err != nil {
log.Printf("Warning: .env file not found")
}
// Initialize client with environment variables
client, err := rownd.NewClient(
rownd.WithAppKey(os.Getenv("ROWND_APP_KEY")),
rownd.WithAppSecret(os.Getenv("ROWND_APP_SECRET")),
rownd.WithBaseURL(os.Getenv("ROWND_BASE_URL")),
)
if err != nil {
log.Fatal(err)
}
}
Environment Files
- Add
.env
to your .gitignore
:
- For testing, create a separate
.env.test
:
# .env.test
ROWND_TEST_APP_KEY=test_key_here
ROWND_TEST_APP_SECRET=test_secret_here
ROWND_TEST_APP_ID=test_app_id_here
ROWND_TEST_BASE_URL=https://api.rownd.io
- Load different env files based on environment:
func loadEnv() {
env := os.Getenv("GO_ENV")
if env == "test" {
godotenv.Load(".env.test")
}
}
License
This project is licensed under the MIT License - see the LICENSE file for details.