Skip to main content

Storage

Token persistence and storage options for the Cocobase Go SDK.

Table of Contents

Overview

Storage provides a way to persist authentication tokens and user data across application restarts. This is essential for maintaining user sessions.

Why Use Storage?

Without storage:

  • Users must log in every time the application starts
  • Authentication tokens are lost on restart

With storage:

  • Tokens persist across restarts
  • Automatic session restoration
  • Better user experience

Storage Interface

All storage implementations must satisfy this interface:

type Storage interface {
Get(key string) (string, error)
Set(key string, value string) error
Delete(key string) error
}

Standard Keys

The SDK uses these keys:

  • cocobase-token: Authentication token
  • cocobase-user: Cached user data (JSON)

Memory Storage

In-memory storage that persists only while the application is running.

When to Use

  • Development and testing
  • Short-lived applications
  • When disk persistence isn't needed
  • Temporary sessions

Creating Memory Storage

import "github.com/lordace-coder/cocobase-go/storage"

store := storage.NewMemoryStorage()

client := cocobase.NewClient(cocobase.Config{
APIKey: "your-api-key",
Storage: store,
})

Complete Example

package main

import (
"context"
"fmt"
"log"

"github.com/lordace-coder/cocobase-go/cocobase"
"github.com/lordace-coder/cocobase-go/storage"
)

func main() {
// Create memory storage
store := storage.NewMemoryStorage()

// Create client
client := cocobase.NewClient(cocobase.Config{
APIKey: "your-api-key",
Storage: store,
})

ctx := context.Background()

// Login (token will be stored in memory)
err := client.Login(ctx, "user@example.com", "password")
if err != nil {
log.Fatal(err)
}

fmt.Println("Logged in successfully")

// Token persists in memory until app exits
if client.IsAuthenticated() {
fmt.Println("Still authenticated")
}
}

Limitations

  • Data lost when application exits
  • Not shared between process instances
  • No cross-session persistence

File Storage

Persistent storage that saves data to a JSON file on disk.

When to Use

  • Production applications
  • Desktop applications
  • CLI tools
  • When session persistence is required
  • Offline-capable applications

Creating File Storage

import "github.com/lordace-coder/cocobase-go/storage"

// Create file storage
store, err := storage.NewFileStorage(".cocobase/auth.json")
if err != nil {
log.Fatal(err)
}

client := cocobase.NewClient(cocobase.Config{
APIKey: "your-api-key",
Storage: store,
})

File Structure

The storage file is a JSON object:

{
"cocobase-token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"cocobase-user": "{\"id\":\"123\",\"email\":\"user@example.com\"...}"
}

Complete Example

package main

import (
"context"
"fmt"
"log"

"github.com/lordace-coder/cocobase-go/cocobase"
"github.com/lordace-coder/cocobase-go/storage"
)

func main() {
ctx := context.Background()

// Create file storage
store, err := storage.NewFileStorage(".cocobase/auth.json")
if err != nil {
log.Fatal(err)
}

// Create client
client := cocobase.NewClient(cocobase.Config{
APIKey: "your-api-key",
Storage: store,
})

// Try to restore existing session
err = client.InitAuth(ctx)
if err != nil {
// No existing session, need to login
fmt.Println("No existing session, please login")

err = client.Login(ctx, "user@example.com", "password")
if err != nil {
log.Fatal(err)
}

fmt.Println("Logged in and session saved")
} else {
fmt.Println("Session restored from file")
}

// Get current user
user, err := client.GetCurrentUser(ctx)
if err != nil {
log.Fatal(err)
}

fmt.Printf("Welcome, %s!\n", user.Email)
}

Session Restoration

func restoreOrLogin(client *cocobase.Client, ctx context.Context) error {
// Try to restore existing session
err := client.InitAuth(ctx)
if err == nil {
// Session restored
user, err := client.GetCurrentUser(ctx)
if err == nil {
fmt.Printf("Welcome back, %s!\n", user.Email)
return nil
}
}

// No valid session, need to login
fmt.Println("Please log in:")

// Get credentials (from user input, config, etc.)
email := getEmail()
password := getPassword()

err = client.Login(ctx, email, password)
if err != nil {
return fmt.Errorf("login failed: %w", err)
}

fmt.Println("Logged in successfully")
return nil
}

File Location

Choose appropriate locations based on OS:

import (
"os"
"path/filepath"
)

func getStoragePath() string {
homeDir, _ := os.UserHomeDir()

// Linux/Mac: ~/.cocobase/auth.json
// Windows: C:\Users\username\.cocobase\auth.json
return filepath.Join(homeDir, ".cocobase", "auth.json")
}

store, err := storage.NewFileStorage(getStoragePath())

Handling File Errors

store, err := storage.NewFileStorage(".cocobase/auth.json")
if err != nil {
log.Printf("Failed to create file storage: %v\n", err)

// Fallback to memory storage
store = storage.NewMemoryStorage()
log.Println("Using memory storage as fallback")
}

client := cocobase.NewClient(cocobase.Config{
APIKey: "your-api-key",
Storage: store,
})

Custom Storage

Implement your own storage backend for specialized needs.

Implementing the Interface

type CustomStorage struct {
// Your fields
}

func (s *CustomStorage) Get(key string) (string, error) {
// Your implementation
return "", nil
}

func (s *CustomStorage) Set(key string, value string) error {
// Your implementation
return nil
}

func (s *CustomStorage) Delete(key string) error {
// Your implementation
return nil
}

Redis Storage Example

package main

import (
"context"
"fmt"

"github.com/go-redis/redis/v8"
)

type RedisStorage struct {
client *redis.Client
prefix string
}

func NewRedisStorage(addr, prefix string) *RedisStorage {
return &RedisStorage{
client: redis.NewClient(&redis.Options{
Addr: addr,
}),
prefix: prefix,
}
}

func (s *RedisStorage) Get(key string) (string, error) {
val, err := s.client.Get(context.Background(), s.prefix+key).Result()
if err == redis.Nil {
return "", fmt.Errorf("key not found: %s", key)
}
return val, err
}

func (s *RedisStorage) Set(key string, value string) error {
return s.client.Set(context.Background(), s.prefix+key, value, 0).Err()
}

func (s *RedisStorage) Delete(key string) error {
return s.client.Del(context.Background(), s.prefix+key).Err()
}

// Usage
func main() {
store := NewRedisStorage("localhost:6379", "cocobase:")

client := cocobase.NewClient(cocobase.Config{
APIKey: "your-api-key",
Storage: store,
})
}

Database Storage Example

package main

import (
"database/sql"
"fmt"

_ "github.com/lib/pq"
)

type DBStorage struct {
db *sql.DB
userID string
}

func NewDBStorage(connStr, userID string) (*DBStorage, error) {
db, err := sql.Open("postgres", connStr)
if err != nil {
return nil, err
}

// Create table if not exists
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS storage (
user_id TEXT,
key TEXT,
value TEXT,
PRIMARY KEY (user_id, key)
)
`)
if err != nil {
return nil, err
}

return &DBStorage{
db: db,
userID: userID,
}, nil
}

func (s *DBStorage) Get(key string) (string, error) {
var value string
err := s.db.QueryRow(
"SELECT value FROM storage WHERE user_id = $1 AND key = $2",
s.userID, key,
).Scan(&value)

if err == sql.ErrNoRows {
return "", fmt.Errorf("key not found: %s", key)
}

return value, err
}

func (s *DBStorage) Set(key string, value string) error {
_, err := s.db.Exec(`
INSERT INTO storage (user_id, key, value)
VALUES ($1, $2, $3)
ON CONFLICT (user_id, key)
DO UPDATE SET value = $3
`, s.userID, key, value)

return err
}

func (s *DBStorage) Delete(key string) error {
_, err := s.db.Exec(
"DELETE FROM storage WHERE user_id = $1 AND key = $2",
s.userID, key,
)
return err
}

// Usage
func main() {
store, err := NewDBStorage("postgres://...", "user-123")
if err != nil {
log.Fatal(err)
}

client := cocobase.NewClient(cocobase.Config{
APIKey: "your-api-key",
Storage: store,
})
}

Encrypted Storage Example

package main

import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"io"

"github.com/lordace-coder/cocobase-go/storage"
)

type EncryptedStorage struct {
inner storage.Storage
cipher cipher.AEAD
}

func NewEncryptedStorage(inner storage.Storage, key []byte) (*EncryptedStorage, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}

aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}

return &EncryptedStorage{
inner: inner,
cipher: aesgcm,
}, nil
}

func (s *EncryptedStorage) Get(key string) (string, error) {
encrypted, err := s.inner.Get(key)
if err != nil {
return "", err
}

data, err := base64.StdEncoding.DecodeString(encrypted)
if err != nil {
return "", err
}

nonceSize := s.cipher.NonceSize()
nonce, ciphertext := data[:nonceSize], data[nonceSize:]

plaintext, err := s.cipher.Open(nil, nonce, ciphertext, nil)
if err != nil {
return "", err
}

return string(plaintext), nil
}

func (s *EncryptedStorage) Set(key string, value string) error {
nonce := make([]byte, s.cipher.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return err
}

ciphertext := s.cipher.Seal(nonce, nonce, []byte(value), nil)
encrypted := base64.StdEncoding.EncodeToString(ciphertext)

return s.inner.Set(key, encrypted)
}

func (s *EncryptedStorage) Delete(key string) error {
return s.inner.Delete(key)
}

// Usage
func main() {
// Create base storage
fileStore, _ := storage.NewFileStorage(".cocobase/auth.json")

// Wrap with encryption
key := []byte("32-byte-encryption-key-here!!!!!")
encryptedStore, err := NewEncryptedStorage(fileStore, key)
if err != nil {
log.Fatal(err)
}

client := cocobase.NewClient(cocobase.Config{
APIKey: "your-api-key",
Storage: encryptedStore,
})
}

Best Practices

1. Use File Storage in Production

// Good: Persistent storage for production
store, err := storage.NewFileStorage(".cocobase/auth.json")
if err != nil {
log.Fatal(err)
}

2. Implement Error Handling

// Good: Handle storage errors gracefully
store, err := storage.NewFileStorage(".cocobase/auth.json")
if err != nil {
log.Printf("File storage failed: %v\n", err)
store = storage.NewMemoryStorage() // Fallback
}

3. Use Appropriate Locations

// Good: Use OS-appropriate paths
import (
"os"
"path/filepath"
)

func getStoragePath() string {
homeDir, _ := os.UserHomeDir()
return filepath.Join(homeDir, ".cocobase", "auth.json")
}

4. Secure Storage Files

// Good: Set appropriate file permissions
store, err := storage.NewFileStorage(".cocobase/auth.json")
if err != nil {
log.Fatal(err)
}

// On Unix systems, restrict access
os.Chmod(".cocobase/auth.json", 0600) // Owner read/write only

5. Initialize Auth on Startup

// Good: Try to restore session on startup
err := client.InitAuth(ctx)
if err != nil {
// No existing session, prompt for login
promptForLogin()
}

6. Clear Storage on Logout

// Good: Logout clears storage automatically
err := client.Logout()
if err != nil {
log.Printf("Logout error: %v\n", err)
}
// Storage is now empty

7. Use Environment-Specific Storage

// Good: Different storage for different environments
func getStorage() cocobase.Storage {
if os.Getenv("ENV") == "production" {
store, _ := storage.NewFileStorage("/var/lib/app/auth.json")
return store
}
return storage.NewMemoryStorage()
}

8. Implement Cleanup

// Good: Clean up resources
store, err := storage.NewFileStorage(".cocobase/auth.json")
if err != nil {
log.Fatal(err)
}

// If your storage needs cleanup
defer func() {
if closer, ok := store.(io.Closer); ok {
closer.Close()
}
}()

Previous: Real-time Updates | Next: Error Handling