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.
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
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}
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}
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}
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}
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}
Mau uses Schema.org vocabulary for structured content. Here are common types:
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}
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}
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}
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}
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}
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}
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}
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}
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}
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}
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}
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}
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}
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}
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"
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}
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}
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}
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}
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}
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}
Need Help? Join the discussion at github.com/mau-network/mau/discussions