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.
How Viewer Links Work
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.
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 )
}
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.
Token Generation
Each link receives a unique UUID token: func GenerateToken () string {
return uuid . New (). String ()
}
Example token: a7f2c4e8-9b3d-4f1a-8e5c-2d6f9a1b3c4e
Link Sharing
The generated link is returned to the user: {
"viewer_url" : "https://api.privycode.com/view/a7f2c4e8-9b3d-4f1a-8e5c-2d6f9a1b3c4e"
}
ViewerLink Model
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.
Creating Links with Custom Expiration
{
"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
MaxViews Behavior 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:
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 )
}
Managing Viewer Links
Update Link Settings
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
}'
Delete Viewer Links
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
}
Link Status Hierarchy
Viewer links are validated in the following order:
Token Validity
Verify the token exists in the database
Deletion Status
Check if the link has been soft-deleted
Time Expiration
Verify the current time is before ExpiresAt
View Limit
Check if ViewCount < MaxViews (when MaxViews > 0)
Best Practices
Choose appropriate expiration times
Quick reviews: 1-3 days
Ongoing projects: 7-14 days
Long-term access: 30+ days (use with view limits)
Set view limits for sensitive content
Single-use links: max_views: 1
Small teams: max_views: 10-50
Larger audiences: max_views: 100+
Public sharing: max_views: 0 (unlimited)
Revoke access immediately when needed
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