Skip to main content
Viewer links are the core feature of PrivyCode, allowing you to share private GitHub repositories with time-limited and view-limited access without granting full repository permissions. Viewer links act as secure proxies between viewers and your private GitHub repositories. Each link contains a unique token that grants temporary access to a specific repository.
1

Link Generation

Authenticated users create viewer links for their repositories:
internal/handlers/viewer_handler.go
func GenerateViewerLinkHandler(w http.ResponseWriter, r *http.Request) {
    user := middleware.GetUserFromContext(r)
    
    var req ViewerLinkRequest
    json.NewDecoder(r.Body).Decode(&req)
    
    // Generate unique token
    token := utils.GenerateToken()
    
    // Calculate expiration
    days := req.ExpiresIn
    if days <= 0 {
        days = 3 // default 3 days
    }
    expiration := time.Now().Add(time.Duration(days) * 24 * time.Hour)
    
    link := models.ViewerLink{
        RepoName:  req.RepoName,
        UserID:    user.ID,
        Token:     token,
        ExpiresAt: expiration,
        MaxViews:  req.MaxViews,
        ViewCount: 0,
    }
    
    config.DB.Create(&link)
}
2

Repository Validation

Before creating a link, the system verifies repository access:
internal/handlers/viewer_handler.go
// Verify repository exists and is accessible
apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s", 
    user.GitHubUsername, req.RepoName)
reqGitHub, _ := http.NewRequest("GET", apiURL, nil)
reqGitHub.Header.Set("Authorization", "token "+user.GitHubToken)
reqGitHub.Header.Set("Accept", "application/vnd.github.v3+json")

client := &http.Client{}
resp, err := client.Do(reqGitHub)

if err != nil || resp.StatusCode != 200 {
    http.Error(w, "Repository not found or inaccessible", http.StatusNotFound)
    return
}
Links can only be created for repositories the authenticated user has access to.
3

Token Generation

Each link receives a unique UUID token:
internal/utils/token.go
func GenerateToken() string {
    return uuid.New().String()
}
Example token: a7f2c4e8-9b3d-4f1a-8e5c-2d6f9a1b3c4e
4

Link Sharing

The generated link is returned to the user:
{
  "viewer_url": "https://api.privycode.com/view/a7f2c4e8-9b3d-4f1a-8e5c-2d6f9a1b3c4e"
}
Viewer links are stored with comprehensive tracking data:
internal/models/viewer_link.go
type ViewerLink struct {
    gorm.Model
    RepoName  string    `gorm:"not null" json:"repo_name"`
    UserID    uint      `gorm:"not null" json:"user_id"`
    User      User      `gorm:"constraint:OnDelete:CASCADE"`
    Token     string    `gorm:"not null;unique" json:"token"`
    ExpiresAt time.Time `json:"expires_at"`
    MaxViews  int       `json:"max_views"`
    ViewCount int       `json:"view_count"`
}
Fields:
  • RepoName - Name of the GitHub repository
  • UserID - Owner of the viewer link
  • Token - Unique access token (UUID)
  • ExpiresAt - Link expiration timestamp
  • MaxViews - Maximum allowed views (0 = unlimited)
  • ViewCount - Current number of views

Expiration Behavior

Time-Based Expiration

Viewer links automatically expire after the specified duration:
internal/handlers/viewer_handler.go
// Check if link has expired
if time.Now().After(link.ExpiresAt) {
    http.Error(w, "Link has expired", http.StatusForbidden)
    return
}
Default Expiration: If not specified, links expire after 3 days.
{
  "repo_name": "my-private-repo",
  "expires_in_days": 7,
  "max_views": 0
}

View Limits

How View Limits Work

Each time a viewer accesses the link, the view count increments:
internal/handlers/viewer_handler.go
func ViewerAccessHandler(w http.ResponseWriter, r *http.Request) {
    var link models.ViewerLink
    dbInstance.Where("token = ?", token).First(&link)
    
    // Check expiration
    if time.Now().After(link.ExpiresAt) {
        http.Error(w, "Link has expired", http.StatusForbidden)
        return
    }
    
    // Check max views
    if link.MaxViews > 0 && link.ViewCount >= link.MaxViews {
        http.Error(w, "View limit reached", http.StatusForbidden)
        return
    }
    
    // Increment view count
    link.ViewCount++
    dbInstance.Save(&link)
    
    // Grant access...
}

View Limit Examples

MaxViewsBehavior
0Unlimited views until expiration
1Single-use link (one-time access)
10Allows exactly 10 views
100Suitable for small teams
Once a view limit is reached, the link becomes permanently inaccessible, even if not expired.

Accessing Repository Contents

Viewer links support three types of access:

1. Repository Root Access

View the repository’s root directory structure:
GET /view/{token}
internal/handlers/viewer_handler.go
// Fetch repository contents from GitHub
apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/contents", 
    user.GitHubUsername, link.RepoName)
req.Header.Set("Authorization", "token "+user.GitHubToken)

client := &http.Client{}
resp, err := client.Do(req)

var contents []struct {
    Name string `json:"name"`
    Type string `json:"type"`
    Path string `json:"path"`
    URL  string `json:"url"`
}
json.NewDecoder(resp.Body).Decode(&contents)

2. File Content Access

Retrieve specific file contents:
GET /view-files/{token}?path=src/main.go
internal/handlers/viewer_handler.go
func ViewFileHandler(w http.ResponseWriter, r *http.Request) {
    token := extractTokenFromPath(r.URL.Path)
    path := r.URL.Query().Get("path")
    
    var link models.ViewerLink
    dbInstance.Where("token = ?", token).First(&link)
    
    // Validate expiration and limits
    if time.Now().After(link.ExpiresAt) {
        http.Error(w, "This link has expired", http.StatusForbidden)
        return
    }
    
    if link.MaxViews > 0 && link.ViewCount >= link.MaxViews {
        http.Error(w, "View limit reached", http.StatusForbidden)
        return
    }
    
    // Fetch file from GitHub
    apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/contents/%s", 
        user.GitHubUsername, link.RepoName, path)
    req.Header.Set("Accept", "application/vnd.github.v3.raw")
    
    // Return raw file content
}

3. Folder Browsing

Navigate through subdirectories:
GET /view-folder/{token}?path=src/components
internal/handlers/viewer_handler.go
func ViewerFolderHandler(w http.ResponseWriter, r *http.Request) {
    token := strings.TrimPrefix(r.URL.Path, "/view-folder/")
    path := r.URL.Query().Get("path")
    
    var link models.ViewerLink
    dbInstance.Where("token = ?", token).First(&link)
    
    // Validate access
    if time.Now().After(link.ExpiresAt) || 
       (link.MaxViews > 0 && link.ViewCount >= link.MaxViews) {
        http.Error(w, "Link expired or view limit reached", http.StatusForbidden)
        return
    }
    
    // Fetch folder contents from GitHub API
    apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/contents/%s", 
        user.GitHubUsername, link.RepoName, path)
}
Modify expiration or view limits for existing links:
internal/handlers/viewer_handler.go
func UpdateViewerLinkHandler(w http.ResponseWriter, r *http.Request) {
    var link models.ViewerLink
    db.First(&link, id)
    
    var payload struct {
        ExpiresInDays int `json:"expires_in_days"`
        MaxViews      int `json:"max_views"`
    }
    json.NewDecoder(r.Body).Decode(&payload)
    
    if payload.ExpiresInDays > 0 {
        link.ExpiresAt = time.Now().Add(
            time.Duration(payload.ExpiresInDays) * 24 * time.Hour)
    }
    
    if payload.MaxViews > 0 {
        link.MaxViews = payload.MaxViews
    }
    
    db.Save(&link)
}
Request:
curl -X PUT https://api.privycode.com/update-link/123 \
  -H "Authorization: Bearer gho_xxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "expires_in_days": 14,
    "max_views": 200
  }'
Soft-delete links to immediately revoke access:
internal/handlers/viewer_handler.go
func DeleteViewerLinkHandler(w http.ResponseWriter, r *http.Request) {
    var link models.ViewerLink
    db.First(&link, id)
    
    // Soft delete (sets DeletedAt timestamp)
    db.Delete(&link)
    
    json.NewEncoder(w).Encode(map[string]string{
        "message": "Viewer link deleted",
    })
}
Deleted links are soft-deleted using GORM’s built-in soft delete feature. Attempts to access deleted links return a 410 Gone status.

Soft Delete Handling

internal/handlers/viewer_handler.go
// Check for soft-deleted links
var link models.ViewerLink
dbInstance.Unscoped().Where("token = ?", token).First(&link)

if link.DeletedAt.Valid {
    http.Error(w, "This link has been deleted", http.StatusGone)
    return
}
Viewer links are validated in the following order:
1

Token Validity

Verify the token exists in the database
2

Deletion Status

Check if the link has been soft-deleted
3

Time Expiration

Verify the current time is before ExpiresAt
4

View Limit

Check if ViewCount < MaxViews (when MaxViews > 0)

Best Practices

  • Quick reviews: 1-3 days
  • Ongoing projects: 7-14 days
  • Long-term access: 30+ days (use with view limits)
  • Single-use links: max_views: 1
  • Small teams: max_views: 10-50
  • Larger audiences: max_views: 100+
  • Public sharing: max_views: 0 (unlimited)
Use the delete endpoint to immediately invalidate links:
curl -X DELETE https://api.privycode.com/delete-link/123 \
  -H "Authorization: Bearer gho_xxxx"
Track link usage through the dashboard to understand access patterns and adjust limits accordingly.

Next Steps

Authentication

Learn about GitHub OAuth and token-based auth

Security

Explore security features and best practices

Build docs developers (and LLMs) love