OctoTown — Specification v3.0

A decentralized, public-only social network using GitHub as storage, with profile caching via GitHub Actions.

Overview

  • Target users: GitHub users only
  • Storage: GitHub Issues as posts, GitHub's native follow system for social graph
  • Identity: User's .social repository (e.g., github.com/username/.social)
  • Feed: Fetched directly from GitHub API (Issues from followed users' .social repos)
  • Social Graph: Uses GitHub's REST API (GET /users/{username}/following) to determine who a user follows

Repository Structure: <username>/.social

.social/
├── README.md                              # Repository description
├── following/
│   └── <username>.yml                     # Cached profile for each followed user
├── hidden/
│   └── <username>                         # Hidden users (excluded from feed)
├── hidden-threads/
│   └── <user>-<issue_id>                  # Hidden threads (excluded from feed)
├── muted/
│   └── <user>-<issue_id>                  # Muted threads (no notifications)
└── notifications/
    ├── .last                              # ISO 8601 timestamp of last notification check
    └── <timestamp>-<type>-<user>-<id>.yml # Notification entries

Following Cache (following/<username>.yml)

Each followed user has their own profile file in the following/ directory, auto-generated by the profile-sync workflow. This allows front-end apps to read cached profile data instead of making quota-restricted API calls.

File: following/alice.yml

# Profile for alice - auto-generated by feed-sync workflow
last_updated: 2026-01-27T12:00:00Z
login: "alice"
name: "Alice Smith"
bio: "Software developer and open source enthusiast"
location: "San Francisco, CA"
blog: "https://alice.dev"
avatar_url: "https://avatars.githubusercontent.com/u/12345"
followers: 1234
following: 567
twitter_username: "alicedev"
company: "Acme Corp"
has_social_repo: true
Field Description
last_updated When the profile was last fetched from GitHub API
login GitHub username
name Display name
bio User bio
location Geographic location
blog Personal website URL
avatar_url Profile picture URL
followers Follower count
following Following count
twitter_username Twitter/X handle
company Company/organization
has_social_repo Whether user has a .social repository

Caching Strategy:

  • Each profile has its own last_updated timestamp
  • Profiles are refreshed if older than 24 hours
  • New followed users are fetched immediately
  • Unfollowed users have their profile files automatically removed
  • This reduces API usage significantly for stable following lists

Note: Only users with has_social_repo: true will have their posts synced to the feed.

Social Graph

OctoTown leverages GitHub's native follow system instead of maintaining separate files:

Action GitHub API
Get following GET /users/{username}/following
Get followers GET /users/{username}/followers
Follow user PUT /user/following/{username}
Unfollow user DELETE /user/following/{username}
Check if following GET /user/following/{username}

This approach:

  • Eliminates the need for manually managing social graph files (no followers.md or follow PRs)
  • Uses the following/ directory only for cached profile data (auto-generated by feed-sync)
  • Removes the complexity of auto-accepting follower PRs
  • Uses GitHub's existing infrastructure for social connections
  • Provides real-time follower/following counts via the API

Muted Threads (muted/<user>-<issue_id>)

Users can mute specific threads to stop receiving notifications for them. Each muted thread is represented by an empty file in the muted/ directory.

Filename pattern: <user>-<issue_id>

Example: muted/alice-42 (mutes thread https://github.com/alice/.social/issues/42)

Operation Action
Mute Create empty file muted/<user>-<issue_id>
Unmute Delete file muted/<user>-<issue_id>
Check File exists = muted

Client behavior:

  • Muted threads should not trigger notifications
  • Muted threads still appear in feed (muting ≠ hiding)
  • UI should indicate when viewing a muted thread
  • Provide easy toggle to mute/unmute from thread view

Hidden Threads (hidden-threads/<user>-<issue_id>)

Users can hide specific threads to completely remove them from their feed. Each hidden thread is represented by an empty file in the hidden-threads/ directory.

Filename pattern: <user>-<issue_id>

Example: hidden-threads/alice-42 (hides thread https://github.com/alice/.social/issues/42)

Operation Action
Hide Create empty file hidden-threads/<user>-<issue_id>
Unhide Delete file hidden-threads/<user>-<issue_id>
Check File exists = hidden

Feed sync behavior:

  • The feed-sync workflow should skip posts that match a hidden thread
  • Existing feed files for hidden threads may be deleted or left to expire

Client behavior:

  • Hidden threads should not appear in the feed view
  • Provide a way to view and manage hidden threads
  • Hiding a thread also implicitly mutes it (no notifications)

Notifications (notifications/)

Notifications alert users about activity relevant to them. Notifications are stored as YAML files in the notifications/ directory.

Filename pattern: <timestamp>-<type>-<user>-<id>.yml

Example: notifications/2026-01-27T12-00-00Z-reply-alice-42.yml

Notification Types

Type Trigger Description
reply Comment on your post Someone replied to your post
repost Repost of your post Someone reposted your content
quote Quote of your post Someone quoted your content
mention @username in post/comment Someone mentioned you
follow New follower with .social repo An OctoTown user followed you

Notification File Format

type: reply
timestamp: 2026-01-27T12:00:00Z
read: false
actor: alice
actor_avatar: https://avatars.githubusercontent.com/u/12345
target_url: https://github.com/you/.social/issues/42
target_title: "post: My awesome post"
content: "Great post! I totally agree."
Field Description
type Notification type (reply, repost, quote, mention, follow)
timestamp When the activity occurred
read Whether the user has seen this notification
actor Username who triggered the notification
actor_avatar Avatar URL of the actor
target_url URL of the relevant post/issue
target_title Title of the target post (for context)
content Preview of the content (for replies/quotes)

Notification Generation

The feed-sync workflow should generate notifications for:

  1. Replies: New comments on the owner's posts
  2. Reposts/Quotes: New issues referencing the owner's posts
  3. Mentions: Posts/comments containing @owner

Notifications are not generated for:

  • Muted threads (check muted/ directory)
  • Hidden threads (check hidden-threads/ directory)
  • The owner's own activity

Client Behavior

  • Display unread notification count as a badge
  • Provide a notifications view to browse all notifications
  • Mark notifications as read when viewed
  • Allow clearing/dismissing notifications

User Profile

OctoTown uses GitHub's REST API to fetch user profile data instead of maintaining a separate profile file:

GET /users/{username}

API Documentation: https://docs.github.com/en/rest/users/users#get-a-user

GitHub Field OctoTown Usage
login Username / handle
name Display name
bio User tagline / short bio
location Geographic location
blog Personal website URL
avatar_url Profile picture
followers Follower count
following Following count
twitter_username Twitter/X handle
company Company/organization

This approach:

  • Eliminates the need for a profile.md file
  • Keeps profile data in sync with the user's GitHub profile
  • Reduces repository complexity
  • Leverages GitHub's existing profile infrastructure

Post Format Conventions

Type Title Format Content Limit
Post post: <text> 250 chars (256 - 6 for prefix)
Repost repost: <issue_url> Empty body = pure retweet
Quote repost: <issue_url> First 250 chars of body = plain text quote
Reply Comment on Issue 250 chars

Labels

Posts can have labels (similar to Reddit flairs) using GitHub's native issue labels feature. Labels help categorize posts and make them easier to discover.

Aspect Description
Storage GitHub Issue labels
Who can add Post owner only (at creation or via edit)
Format Any valid GitHub label name
Display Clients should display labels as colored badges

Label Object Structure:

interface Label {
  name: string;        // Label name (e.g., "announcement")
  color: string;       // Hex color without # (e.g., "0366d6")
  description?: string; // Optional description
}

Common label examples:

  • question - Asking for help or opinions
  • announcement - Important updates
  • discussion - Open conversation
  • showcase - Sharing work or projects
  • offtopic - Non-technical content

API for labels:

POST /repos/{owner}/.social/issues
{
  "title": "post: My awesome post",
  "labels": ["announcement", "showcase"]
}

Note: Labels must exist in the repository before they can be used. The .social-template repository should include common labels. Users can create custom labels via GitHub's UI or API.

Front-end Implementation Guidelines

Displaying labels:

Label Count Display Behavior
0 No labels shown
1 Show label directly as a colored badge
2+ Show a "Labels..." button that reveals all labels on hover

Color contrast: Clients should calculate text color based on background brightness to ensure readability:

function getTextColor(hexColor) {
  const r = parseInt(hexColor.substr(0, 2), 16);
  const g = parseInt(hexColor.substr(2, 2), 16);
  const b = parseInt(hexColor.substr(4, 2), 16);
  const brightness = (r * 299 + g * 587 + b * 114) / 1000;
  return brightness > 128 ? '#000000' : '#ffffff';
}

Parsing labels from feed files: Labels in feed files may be stored as either strings or objects. Clients must handle both formats:

function parseLabel(label) {
  if (typeof label === 'string') {
    return { name: label, color: '888888' }; // default gray
  }
  return {
    name: label.name,
    color: label.color || '888888',
    description: label.description
  };
}

Examples

Post:

Title: post: Just shipped a new feature! Check it out 🚀
Body: (optional extended content, media, Markdown)
Labels: ["announcement"]

Repost (pure retweet):

Title: repost: https://github.com/alice/.social/issues/42
Body: (empty)

Quote repost:

Title: repost: https://github.com/alice/.social/issues/42
Body: This is so true! Everyone should read this.
(first 250 chars must be plain text, remainder can include Markdown/media)

Feed

The feed is fetched directly from the GitHub API. Clients query Issues from the .social repositories of followed users in real-time.

Feed Fetching (Client-Side)

  1. Get list of followed users via GET /users/{owner}/following
  2. For each followed user (skip if in ./hidden/):
    • Check if user has a .social repository (cached in ./following/<username>.yml)
    • GET /repos/<user>/.social/issues?state=open&sort=created&direction=desc
    • Skip malformed issues (no valid post: or repost: prefix)
    • Skip issues matching ./hidden-threads/<user>-<issue_id>
  3. Include the owner's own posts from their .social repository
  4. For reposts/quotes: resolve chain to original post
  5. Sort by most recent activity (post creation or latest reply)

Extended Content

Posts can have extended content in the Issue body beyond the 250-character title. Clients should:

  • Display a "Show more" button when issue.body is non-empty
  • Lazy-load the full body content when clicked (via GET /repos/{owner}/.social/issues/{id})
  • Support Markdown rendering for extended content

GitHub Actions

Profile Sync (Scheduled: every 30 minutes)

  1. Fetch the owner's following list via GET /users/{owner}/following
  2. For each followed user:
    • Check if profile cache exists in ./following/<username>.yml
    • Skip if cache is less than 24 hours old
    • Fetch profile via GET /users/<username>
    • Check if user has a .social repository
    • Write profile cache to ./following/<username>.yml
  3. Remove profile files for users no longer followed
  4. Commit and push if changes exist

Interaction Contracts

Action Mechanism Validation
Post Issue titled post: <text> Owner only; ≤250 chars
Reply Comment on Issue GitHub permissions; ≤250 chars
Like Reaction on Issue/comment GitHub permissions
Follow GitHub API: PUT /user/following/{username} Authenticated user
Unfollow GitHub API: DELETE /user/following/{username} Authenticated user
Hide User Add ./hidden/<user> Hides user from feed
Hide Thread Add ./hidden-threads/<user>-<issue_id> Hides thread from feed
Unhide Thread Remove ./hidden-threads/<user>-<issue_id> Shows thread in feed again
Mute Thread Add ./muted/<user>-<issue_id> Stop notifications for thread
Unmute Thread Remove ./muted/<user>-<issue_id> Resume notifications for thread
Close Post GitHub API: Update issue state: closed Owner only
Lock Post GitHub API: Lock issue with reason Owner only
Unlock Post GitHub API: Unlock issue Owner only
Delete Post GitHub GraphQL: deleteIssue mutation Owner only
Repost Issue titled repost: <url>, empty body Valid .social issue URL
Quote Issue titled repost: <url>, body with text First 250 chars plain text

Post Management Actions

Post owners have additional moderation capabilities for their own posts.

Close Post

Closes the Issue, preventing it from appearing in feeds.

Aspect Description
API PATCH /repos/{owner}/.social/issues/{id} with state: "closed"
Effect on Issue Issue is closed but still visible on GitHub
Effect on Feed Closed issues are not synced; existing feed files should be removed
Reversible Yes, issue can be reopened

Lock Post

Locks the Issue, preventing anyone (except the owner) from adding new comments.

Aspect Description
API PUT /repos/{owner}/.social/issues/{id}/lock
Lock Reasons off-topic, too heated, resolved, spam, or no reason
Effect on Issue No new comments allowed
Effect on Feed Post remains in feed, replies are frozen
Reversible Yes, issue can be unlocked

Unlock Post

Unlocks a previously locked Issue, allowing comments again.

Aspect Description
API DELETE /repos/{owner}/.social/issues/{id}/lock
Effect Comments are allowed again

Delete Post

Permanently deletes the Issue. This action cannot be undone.

Aspect Description
API GraphQL mutation deleteIssue(input: {issueId: $nodeId})
Effect on Issue Issue is permanently deleted from GitHub
Effect on Feed Feed file should be immediately removed from feed/ and feed/archive/
Reversible No - this action is permanent

Client Behavior for Post Actions

When a client performs Close or Delete actions, it should:

  1. Execute the GitHub API call to close/delete the Issue
  2. Delete the corresponding feed file from feed/<timestamp>-<user>-<issue_id>.md
  3. Also check and delete from feed/archive/ if present
  4. Refresh the feed view to reflect the change

This ensures the post is immediately removed from the user's feed without waiting for the next feed-sync workflow run.

URL Patterns

Resource Pattern
User profile https://github.com/<user>/.social
Post (Issue) https://github.com/<user>/.social/issues/<id>
Valid repost URL regex ^https://github\.com/([a-zA-Z0-9-]+)/\.social/issues/(\d+)$

TODO

  • Rate limit handling: Strategy for users following 100+ accounts. GitHub Actions with PAT allows 5,000 requests/hour. Consider batching, dynamic sync intervals, or pagination limits.
  • OctoTown users discovery: Not everyone a user follows on GitHub will have a .social repository. Consider caching which followed users are OctoTown participants.