docs/08-building-social-apps
Wednesday 11 March 2026

Building Social Applications with Mau

This guide provides practical patterns and examples for building decentralized social applications with Mau. Learn how to structure your application, handle common scenarios, and leverage Mau’s features effectively.

Table of Contents

Application Architecture

Basic Structure

A typical Mau social application consists of:

your-app/
├── main.go              # Application entry point
├── client/              # Mau client initialization
├── handlers/            # HTTP request handlers
├── ui/                  # Frontend (web, desktop, mobile)
└── storage/             # Local data cache and indexing

Initialization Pattern

 1package main
 2
 3import (
 4    "log"
 5    "os"
 6    "path/filepath"
 7    
 8    "github.com/mau-network/mau"
 9)
10
11type App struct {
12    client *mau.Client
13    config Config
14}
15
16func NewApp(dataDir string) (*App, error) {
17    // Ensure data directory exists
18    if err := os.MkdirAll(dataDir, 0700); err != nil {
19        return nil, err
20    }
21    
22    // Initialize Mau client
23    client, err := mau.NewClient(dataDir)
24    if err != nil {
25        return nil, err
26    }
27    
28    return &App{
29        client: client,
30        config: LoadConfig(),
31    }, nil
32}
33
34func (a *App) Start() error {
35    // Start HTTP server for peer communication
36    go a.client.StartServer(":8080")
37    
38    // Start DHT for peer discovery
39    go a.client.StartDHT()
40    
41    // Start syncing with known peers
42    go a.client.StartSync()
43    
44    log.Println("Application started successfully")
45    return nil
46}

Common Patterns

1. Post-and-Sync Pattern

The most common pattern: create content and automatically share it with peers.

 1func (a *App) CreatePost(author, content string) error {
 2    post := map[string]interface{}{
 3        "@context": "https://schema.org",
 4        "@type":    "SocialMediaPosting",
 5        "headline": content,
 6        "author": map[string]interface{}{
 7            "@type": "Person",
 8            "name":  author,
 9        },
10        "datePublished": time.Now().Format(time.RFC3339),
11    }
12    
13    // Generate unique filename
14    filename := fmt.Sprintf("post-%d.json", time.Now().Unix())
15    
16    // Save encrypted and signed
17    if err := a.client.Save(filename, post); err != nil {
18        return fmt.Errorf("save post: %w", err)
19    }
20    
21    // Automatically synced with peers
22    return nil
23}

2. Follow-and-Fetch Pattern

Follow users and fetch their content periodically.

 1func (a *App) FollowUser(fingerprint string) error {
 2    // Add to follow list
 3    if err := a.client.Follow(fingerprint); err != nil {
 4        return err
 5    }
 6    
 7    // Fetch their content immediately
 8    return a.fetchUserContent(fingerprint)
 9}
10
11func (a *App) fetchUserContent(fingerprint string) error {
12    // Discover peer address
13    addr, err := a.client.FindPeer(fingerprint)
14    if err != nil {
15        return fmt.Errorf("find peer: %w", err)
16    }
17    
18    // Fetch their public files
19    files, err := a.client.FetchFiles(addr, fingerprint)
20    if err != nil {
21        return fmt.Errorf("fetch files: %w", err)
22    }
23    
24    // Process each file
25    for _, file := range files {
26        if err := a.processFile(file); err != nil {
27            log.Printf("process file %s: %v", file.Name, err)
28        }
29    }
30    
31    return nil
32}

3. Timeline Aggregation Pattern

Combine content from multiple sources into a unified timeline.

 1type TimelineItem struct {
 2    Content   map[string]interface{}
 3    Author    string
 4    Timestamp time.Time
 5    Verified  bool
 6}
 7
 8func (a *App) BuildTimeline(limit int) ([]TimelineItem, error) {
 9    var items []TimelineItem
10    
11    // Get followed users
12    following := a.client.Following()
13    
14    for _, fingerprint := range following {
15        // Fetch recent posts from this user
16        posts, err := a.fetchRecentPosts(fingerprint, 10)
17        if err != nil {
18            log.Printf("fetch posts for %s: %v", fingerprint, err)
19            continue
20        }
21        
22        for _, post := range posts {
23            items = append(items, TimelineItem{
24                Content:   post,
25                Author:    fingerprint,
26                Timestamp: extractTimestamp(post),
27                Verified:  true, // Mau verifies signatures
28            })
29        }
30    }
31    
32    // Sort by timestamp (newest first)
33    sort.Slice(items, func(i, j int) bool {
34        return items[i].Timestamp.After(items[j].Timestamp)
35    })
36    
37    // Limit results
38    if len(items) > limit {
39        items = items[:limit]
40    }
41    
42    return items, nil
43}

4. Private Messaging Pattern

Send encrypted direct messages between users.

 1func (a *App) SendPrivateMessage(recipientFingerprint, message string) error {
 2    dm := map[string]interface{}{
 3        "@context": "https://schema.org",
 4        "@type":    "Message",
 5        "text":     message,
 6        "sender": map[string]interface{}{
 7            "@type": "Person",
 8            "identifier": a.client.Fingerprint(),
 9        },
10        "recipient": map[string]interface{}{
11            "@type": "Person",
12            "identifier": recipientFingerprint,
13        },
14        "dateSent": time.Now().Format(time.RFC3339),
15    }
16    
17    filename := fmt.Sprintf("dm-%s-%d.json", 
18        recipientFingerprint[:8], 
19        time.Now().Unix())
20    
21    // Encrypt specifically for recipient
22    if err := a.client.SaveEncrypted(filename, dm, recipientFingerprint); err != nil {
23        return fmt.Errorf("save message: %w", err)
24    }
25    
26    // Send directly to recipient's server
27    if err := a.client.SendTo(recipientFingerprint, filename); err != nil {
28        return fmt.Errorf("send message: %w", err)
29    }
30    
31    return nil
32}
33
34func (a *App) ReadPrivateMessages() ([]map[string]interface{}, error) {
35    files, err := a.client.List("dm-")
36    if err != nil {
37        return nil, err
38    }
39    
40    var messages []map[string]interface{}
41    for _, filename := range files {
42        var msg map[string]interface{}
43        if err := a.client.Load(filename, &msg); err != nil {
44            log.Printf("load message %s: %v", filename, err)
45            continue
46        }
47        messages = append(messages, msg)
48    }
49    
50    return messages, nil
51}

Content Types

Schema.org Types for Social Apps

Mau uses Schema.org vocabulary for structured content. Here are common types:

Social Media Post

 1post := map[string]interface{}{
 2    "@context": "https://schema.org",
 3    "@type":    "SocialMediaPosting",
 4    "headline": "Post title",
 5    "articleBody": "Full post content with markdown support",
 6    "author": map[string]interface{}{
 7        "@type": "Person",
 8        "name":  "Alice",
 9        "identifier": fingerprint,
10    },
11    "datePublished": "2026-03-11T02:00:00Z",
12    "keywords": []string{"mau", "decentralized", "social"},
13}

Comment/Reply

 1comment := map[string]interface{}{
 2    "@context": "https://schema.org",
 3    "@type":    "Comment",
 4    "text":     "Great post!",
 5    "author": map[string]interface{}{
 6        "@type": "Person",
 7        "name":  "Bob",
 8    },
 9    "dateCreated": "2026-03-11T02:05:00Z",
10    "parentItem": map[string]interface{}{
11        "@type": "SocialMediaPosting",
12        "identifier": "original-post-id",
13    },
14}

Photo/Image Post

 1photo := map[string]interface{}{
 2    "@context": "https://schema.org",
 3    "@type":    "ImageObject",
 4    "name":     "Sunset in Berlin",
 5    "contentUrl": "photos/sunset-2026-03-11.jpg",
 6    "encodingFormat": "image/jpeg",
 7    "author": map[string]interface{}{
 8        "@type": "Person",
 9        "name":  "Alice",
10    },
11    "datePublished": "2026-03-11T18:30:00Z",
12    "caption": "Beautiful sunset from my balcony",
13}

User Profile

 1profile := map[string]interface{}{
 2    "@context": "https://schema.org",
 3    "@type":    "Person",
 4    "name":     "Alice",
 5    "identifier": fingerprint,
 6    "description": "Software engineer interested in decentralization",
 7    "image": "avatar.jpg",
 8    "url": "https://alice.example.com",
 9    "sameAs": []string{
10        "https://github.com/alice",
11        "https://twitter.com/alice",
12    },
13}

Event

 1event := map[string]interface{}{
 2    "@context": "https://schema.org",
 3    "@type":    "Event",
 4    "name":     "Mau Developer Meetup",
 5    "description": "Monthly gathering of Mau developers",
 6    "startDate": "2026-03-15T18:00:00Z",
 7    "endDate": "2026-03-15T20:00:00Z",
 8    "location": map[string]interface{}{
 9        "@type": "Place",
10        "name":  "Berlin Tech Hub",
11        "address": map[string]interface{}{
12            "@type": "PostalAddress",
13            "streetAddress": "Tech Street 123",
14            "addressLocality": "Berlin",
15            "addressCountry": "DE",
16        },
17    },
18    "organizer": map[string]interface{}{
19        "@type": "Person",
20        "name":  "Alice",
21    },
22}

User Interactions

Likes and Reactions

 1func (a *App) LikePost(postID, authorFingerprint string) error {
 2    like := map[string]interface{}{
 3        "@context": "https://schema.org",
 4        "@type":    "LikeAction",
 5        "agent": map[string]interface{}{
 6            "@type": "Person",
 7            "identifier": a.client.Fingerprint(),
 8        },
 9        "object": map[string]interface{}{
10            "@type": "SocialMediaPosting",
11            "identifier": postID,
12            "author": map[string]interface{}{
13                "@type": "Person",
14                "identifier": authorFingerprint,
15            },
16        },
17        "startTime": time.Now().Format(time.RFC3339),
18    }
19    
20    filename := fmt.Sprintf("like-%s-%d.json", postID, time.Now().Unix())
21    return a.client.Save(filename, like)
22}

Shares/Reposts

 1func (a *App) SharePost(originalPost map[string]interface{}) error {
 2    share := map[string]interface{}{
 3        "@context": "https://schema.org",
 4        "@type":    "ShareAction",
 5        "agent": map[string]interface{}{
 6            "@type": "Person",
 7            "identifier": a.client.Fingerprint(),
 8        },
 9        "object": originalPost,
10        "startTime": time.Now().Format(time.RFC3339),
11    }
12    
13    filename := fmt.Sprintf("share-%d.json", time.Now().Unix())
14    return a.client.Save(filename, share)
15}

Following/Unfollowing

 1func (a *App) ToggleFollow(fingerprint string) error {
 2    following := a.client.Following()
 3    
 4    // Check if already following
 5    for _, fp := range following {
 6        if fp == fingerprint {
 7            return a.client.Unfollow(fingerprint)
 8        }
 9    }
10    
11    return a.client.Follow(fingerprint)
12}

Data Synchronization

Periodic Sync

 1func (a *App) StartPeriodicSync(interval time.Duration) {
 2    ticker := time.NewTicker(interval)
 3    defer ticker.Stop()
 4    
 5    for range ticker.C {
 6        if err := a.syncAll(); err != nil {
 7            log.Printf("sync error: %v", err)
 8        }
 9    }
10}
11
12func (a *App) syncAll() error {
13    following := a.client.Following()
14    
15    for _, fingerprint := range following {
16        if err := a.fetchUserContent(fingerprint); err != nil {
17            log.Printf("sync %s: %v", fingerprint, err)
18            // Continue with other users
19        }
20    }
21    
22    return nil
23}

Real-time Notifications

 1func (a *App) WatchForNewContent(callback func(filename string)) error {
 2    watcher, err := fsnotify.NewWatcher()
 3    if err != nil {
 4        return err
 5    }
 6    defer watcher.Close()
 7    
 8    // Watch the Mau data directory
 9    if err := watcher.Add(a.client.DataDir()); err != nil {
10        return err
11    }
12    
13    for {
14        select {
15        case event := <-watcher.Events:
16            if event.Op&fsnotify.Create == fsnotify.Create {
17                callback(event.Name)
18            }
19        case err := <-watcher.Errors:
20            log.Printf("watcher error: %v", err)
21        }
22    }
23}

Selective Sync

 1func (a *App) SyncRecentContent(fingerprint string, since time.Time) error {
 2    files, err := a.client.FetchFiles(fingerprint)
 3    if err != nil {
 4        return err
 5    }
 6    
 7    for _, file := range files {
 8        // Skip old files
 9        if file.ModTime.Before(since) {
10            continue
11        }
12        
13        if err := a.processFile(file); err != nil {
14            log.Printf("process file %s: %v", file.Name, err)
15        }
16    }
17    
18    return nil
19}

Example Applications

1. Microblogging Platform (Twitter-like)

 1package main
 2
 3import (
 4    "github.com/mau-network/mau"
 5)
 6
 7type MicroBlog struct {
 8    client *mau.Client
 9}
10
11func (m *MicroBlog) Post(text string) error {
12    post := map[string]interface{}{
13        "@context": "https://schema.org",
14        "@type":    "SocialMediaPosting",
15        "headline": text,
16        "author": map[string]interface{}{
17            "@type": "Person",
18            "identifier": m.client.Fingerprint(),
19        },
20        "datePublished": time.Now().Format(time.RFC3339),
21    }
22    
23    filename := fmt.Sprintf("post-%d.json", time.Now().Unix())
24    return m.client.Save(filename, post)
25}
26
27func (m *MicroBlog) Timeline(limit int) ([]map[string]interface{}, error) {
28    // Aggregate posts from followed users
29    // (implementation similar to BuildTimeline above)
30}
31
32func (m *MicroBlog) Search(query string) ([]map[string]interface{}, error) {
33    // Search across cached posts
34    // (requires local indexing)
35}

2. Photo Sharing App (Instagram-like)

 1type PhotoApp struct {
 2    client *mau.Client
 3}
 4
 5func (p *PhotoApp) UploadPhoto(filepath, caption string) error {
 6    // Read and encode photo
 7    data, err := os.ReadFile(filepath)
 8    if err != nil {
 9        return err
10    }
11    
12    // Save photo file
13    photoFilename := fmt.Sprintf("photo-%d.jpg", time.Now().Unix())
14    if err := p.client.SaveBinary(photoFilename, data); err != nil {
15        return err
16    }
17    
18    // Create metadata
19    metadata := map[string]interface{}{
20        "@context": "https://schema.org",
21        "@type":    "ImageObject",
22        "contentUrl": photoFilename,
23        "caption": caption,
24        "datePublished": time.Now().Format(time.RFC3339),
25    }
26    
27    metaFilename := fmt.Sprintf("photo-%d.json", time.Now().Unix())
28    return p.client.Save(metaFilename, metadata)
29}
30
31func (p *PhotoApp) Gallery(fingerprint string) ([]map[string]interface{}, error) {
32    // Fetch all ImageObject posts from user
33}

3. Discussion Forum

 1type Forum struct {
 2    client *mau.Client
 3}
 4
 5func (f *Forum) CreateThread(title, content string) error {
 6    thread := map[string]interface{}{
 7        "@context": "https://schema.org",
 8        "@type":    "DiscussionForumPosting",
 9        "headline": title,
10        "articleBody": content,
11        "dateCreated": time.Now().Format(time.RFC3339),
12    }
13    
14    filename := fmt.Sprintf("thread-%d.json", time.Now().Unix())
15    return f.client.Save(filename, thread)
16}
17
18func (f *Forum) Reply(threadID, content string) error {
19    reply := map[string]interface{}{
20        "@context": "https://schema.org",
21        "@type":    "Comment",
22        "text":     content,
23        "parentItem": map[string]interface{}{
24            "@type": "DiscussionForumPosting",
25            "identifier": threadID,
26        },
27        "dateCreated": time.Now().Format(time.RFC3339),
28    }
29    
30    filename := fmt.Sprintf("reply-%s-%d.json", threadID, time.Now().Unix())
31    return f.client.Save(filename, reply)
32}

Best Practices

1. Content Naming

Use descriptive, sortable filenames:

1// Good: chronological and descriptive
2"post-2026-03-11-1234567890.json"
3"photo-sunset-2026-03-11.jpg"
4"dm-abc123-2026-03-11.json"
5
6// Avoid: ambiguous or non-sortable
7"post.json"
8"image.jpg"
9"message-123.json"

2. Error Handling

Always handle errors gracefully, especially for network operations:

 1func (a *App) SafeFetch(fingerprint string) error {
 2    // Set timeout
 3    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
 4    defer cancel()
 5    
 6    // Attempt fetch with retry
 7    for attempt := 0; attempt < 3; attempt++ {
 8        select {
 9        case <-ctx.Done():
10            return ctx.Err()
11        default:
12            if err := a.client.FetchFiles(fingerprint); err == nil {
13                return nil
14            }
15            time.Sleep(time.Second * time.Duration(attempt+1))
16        }
17    }
18    
19    return fmt.Errorf("fetch failed after 3 attempts")
20}

3. Local Caching

Cache remote content to reduce network usage:

 1type Cache struct {
 2    store map[string]CachedItem
 3    mutex sync.RWMutex
 4}
 5
 6type CachedItem struct {
 7    Data      map[string]interface{}
 8    FetchedAt time.Time
 9    TTL       time.Duration
10}
11
12func (c *Cache) Get(key string) (map[string]interface{}, bool) {
13    c.mutex.RLock()
14    defer c.mutex.RUnlock()
15    
16    item, exists := c.store[key]
17    if !exists {
18        return nil, false
19    }
20    
21    // Check if expired
22    if time.Since(item.FetchedAt) > item.TTL {
23        return nil, false
24    }
25    
26    return item.Data, true
27}

4. Incremental Loading

Load content progressively for better UX:

 1func (a *App) LoadTimeline(offset, limit int) ([]TimelineItem, error) {
 2    // Fetch a page of results
 3    allItems := a.getCachedTimeline()
 4    
 5    start := offset
 6    end := offset + limit
 7    
 8    if start >= len(allItems) {
 9        return []TimelineItem{}, nil
10    }
11    
12    if end > len(allItems) {
13        end = len(allItems)
14    }
15    
16    return allItems[start:end], nil
17}

5. Privacy Considerations

Always respect privacy settings:

 1func (a *App) CanView(content map[string]interface{}, viewer string) bool {
 2    // Check if content is public
 3    if isPublic(content) {
 4        return true
 5    }
 6    
 7    // Check if viewer is in recipient list
 8    recipients := extractRecipients(content)
 9    for _, recipient := range recipients {
10        if recipient == viewer {
11            return true
12        }
13    }
14    
15    // Check if viewer is followed by author
16    author := extractAuthor(content)
17    following := a.client.FollowedBy(author)
18    for _, fp := range following {
19        if fp == viewer {
20            return true
21        }
22    }
23    
24    return false
25}

6. Content Validation

Validate content before processing:

 1func validatePost(post map[string]interface{}) error {
 2    // Check required fields
 3    if post["@type"] == nil {
 4        return errors.New("missing @type field")
 5    }
 6    
 7    // Validate Schema.org type
 8    validTypes := []string{
 9        "SocialMediaPosting",
10        "ImageObject",
11        "Comment",
12        "Message",
13    }
14    
15    postType := post["@type"].(string)
16    valid := false
17    for _, t := range validTypes {
18        if postType == t {
19            valid = true
20            break
21        }
22    }
23    
24    if !valid {
25        return fmt.Errorf("invalid @type: %s", postType)
26    }
27    
28    return nil
29}

7. Performance Optimization

Index content for fast retrieval:

 1type Index struct {
 2    byAuthor    map[string][]string // fingerprint -> filenames
 3    byTimestamp map[int64][]string  // timestamp -> filenames
 4    byType      map[string][]string // @type -> filenames
 5}
 6
 7func (idx *Index) Add(filename string, content map[string]interface{}) {
 8    // Index by author
 9    if author := extractAuthor(content); author != "" {
10        idx.byAuthor[author] = append(idx.byAuthor[author], filename)
11    }
12    
13    // Index by timestamp
14    if ts := extractTimestamp(content); !ts.IsZero() {
15        idx.byTimestamp[ts.Unix()] = append(idx.byTimestamp[ts.Unix()], filename)
16    }
17    
18    // Index by type
19    if t, ok := content["@type"].(string); ok {
20        idx.byType[t] = append(idx.byType[t], filename)
21    }
22}
23
24func (idx *Index) FindByAuthor(fingerprint string) []string {
25    return idx.byAuthor[fingerprint]
26}

Next Steps

Additional Resources


Need Help? Join the discussion at github.com/mau-network/mau/discussions