node.you Logo Image node.you - Profile Service

The profile API powers a user's profile service on the node.you platform. Every profile exists as its own isolated instance running in a Docker container with its own mounted SQLite database. It handles posts, comments, media, voting, and identity resolution.

Profiles are designed to be publicly viewable on the web. Anyone with a link that visits your profile page can see your public posts, no authentication necessary.

However, as a node.you service, only user's that you have verified can interact on your profile. Verified interactions include commenting and voting.

Keep in mind that the identities of verified users are protected with comment anonymization. Using a mechanism for identity resolution across the network, only viewers verified by the commenter can see the commenter's true identity.

Architecture

Each profile is an independent FastAPI service provisioned by the node.you API. The main API assigns a subdomain, spins up a Docker container, and routes traffic via nginx. From there, the profile operates autonomously.

+---------------------------------------+
|              node.you API             |
+-------------------+-------------------+
                    |
             +------+------+
             V             V
+------------------+ +------------------+
|  Service         | |  Service         |
|  Server 1        | |  Server 2        |
|                  | |                  |
| +-------------+  | | +--------------+ |
| |Profile:     |  | | |Other Service | |
| |tom.node.you |  | | +--------------+ |
| +-------------+  | |                  |
|                  | | +--------------+ |
| +--------------+ | | |Profile:      | |
| |Other Service | | | |ryan.node.you | |
| +--------------+ | | +--------------+ |
+------------------+ +------------------+

Authentication

Profiles don't use passwords. Authentication relies on Ed25519 signatures produced by the user's node.

There are two sign-in flows:

Manual flow — The user pastes a signed payload into the login dialog. The profile verifies the signature and issues a JWT session.

Challenge flow — The profile generates a challenge and presents a QR code (or link). The user's node scans it and POSTs a signed response to a callback URL. A WebSocket connection delivers the session to the waiting client.

Client                    Profile API       Client's Node
|                             |                        |
| POST:                       |                        |
| /api/auth/challenge         |                        |
|--------------------------->>|                        |
|                             |                        |
| {challenge_id,callback_url} |                        |
|<<---------------------------|                        |
|                             |                        |
| WS:                         |                        |
| /api/auth/challenge/{id}/ws |                        |
|============================>|                        |
|    (waiting for auth...)    |                        |
|                             |  POST callback_url     |
|                             |  (send signature)      |
|                             |<<----------------------|
|                             | Profile verifies user, |
|                             | checks verifications,  |
|                             | creates JWT session    |
|  { session token }          |                        |
|<<===========================|                        |
|                             |                        |

After authentication, the profile checks with the owner's node API (/api/verifications/check) to confirm the user is in the owner's verification list. Only the owner and verified users get session tokens.

Comment Anonymization

Comments are anonymous by default. Each commenter is assigned a deterministic anonymous ID (e.g., 7X3K9M2P) derived from their public key and a per-profile salt. This keeps identities consistent within a profile while preventing cross-profile tracking.

To reveal a commenter's identity, the client calls /api/comments/resolve-identities with the viewer's public key and a list of anonymous IDs. The profile then contacts each commenter's node to check if the viewer is verified:

Viewer's Client       Profile API     Commenter's Node
|                          |       (activistguy.node.you)
| POST:                    |                      |
| resolve-identities       |                      |
| { viewer_key, anon_ids } |                      |
|------------------------>>|                      |
|                          | GET:                 |
|                          | /api/verifications/  |
|                          | check?public_key=... |
|                          |-------------------->>|
|                          |                      |
|                          |  { verified: true }  |
|                          |<<--------------------|
|                          |                      |
| { "7X3K9M2P": "https://  |                      |
|  activistguy.node.you" } |                      |
|<<------------------------|                      |

The profile only reveals the identity if the commenter has verified the viewer. The profile owner always sees all identities. Commenters always see their own.

Media Pipeline

Uploaded media goes through a two-stage process: staging then claiming.

  1. Upload — Files are uploaded to /api/uploads, processed (EXIF stripped, resized, thumbnails generated, video transcoded to H.264), and stored in a temp directory. The client receives an upload_key.
  2. Claim — When creating a post or comment, the client includes upload_keys. The profile moves files from temp to permanent storage and creates attachment records.

Unclaimed uploads expire after 24 hours.

API Reference

Auth

Method Endpoint Auth Description
POST /api/auth/session No Create session (manual signed payload)
GET /api/auth/me No Get profile URL and owner info
POST /api/auth/challenge No Start challenge-based sign-in
GET /api/auth/challenge/{id} No Poll challenge status
WS /api/auth/challenge/{id}/ws No WebSocket — wait for challenge completion
POST /api/auth/challenge/{id}/complete No Callback for node to submit signed payload

POST /api/auth/session

{
  "public_key": "f13CFZY6Yq+RPkY6...",
  "url": "https://alice.node.you",
  "timestamp": "2026-03-15T12:00:00Z",
  "destination": "https://bob.node.you",
  "signature": "v7hT2GqS74CLy/u3..."
}

The signed message format is {timestamp}:{destination}. Returns a JWT session token with access level (is_owner, is_verified).

POST /api/auth/challenge

Returns:

{
  "challenge_id": "550e8400-...",
  "destination": "https://bob.node.you",
  "callback_url": "https://bob.node.you/api/auth/challenge/550e8400-.../complete",
  "expires_at": "2026-03-15T12:05:00Z"
}

Challenges expire after 5 minutes.


Profile

Method Endpoint Auth Description
GET /api/profile No Get public profile
PUT /api/profile Owner Update display name / bio
GET /api/profile/picture No Serve profile picture
PUT /api/profile/picture Owner Upload / replace profile picture
DELETE /api/profile/picture Owner Remove profile picture

GET /api/profile

{
  "display_name": "Bob",
  "bio": "Building things.",
  "picture_url": "/api/profile/picture",
  "updated_at": "2026-03-15T12:00:00Z"
}

PUT /api/profile/picture

Multipart file upload. Accepts JPEG, PNG, WebP, GIF. Max 5 MB.


Posts

Method Endpoint Auth Description
GET /api/posts No* List published posts
GET /api/posts/drafts Owner List drafts with attachments
GET /api/posts/{id} No* Get post with attachments
POST /api/posts Owner Create post (draft or published)
PUT /api/posts/{id} Owner Update post
POST /api/posts/{id}/publish Owner Publish a draft
POST /api/posts/{id}/unpublish Owner Revert to draft
DELETE /api/posts/{id} Owner Delete post and attachments

*Private posts require a verified session.

POST /api/posts

{
  "title": "First post",
  "body": "Hello world.",
  "is_private": false,
  "status": "published",
  "upload_keys": ["550e8400-..."],
  "links": ["https://example.com"]
}

Posts are auto-upvoted by the owner on publish. Attachments are claimed from staged uploads via upload_keys.


Feed

Method Endpoint Auth Description
GET /api/feed No* Get feed of published posts with comment counts
GET /api/feed/{post_id} No* Get post with full threaded comment tree

*Private posts require a verified session.

GET /api/feed

Query params: skip, limit (max 100), sort_by (created_at or vote_score).

Returns posts with comment_count, user_vote, and attachment thumbnails.

GET /api/feed/{post_id}

Query params: comment_sort (created_at or vote_score).

Returns the post with a fully threaded, anonymized comment tree. Top-level comments sort by the chosen field; replies always sort chronologically.


Comments

Method Endpoint Auth Description
GET /api/posts/{post_id}/comments No List anonymized comments
POST /api/posts/{post_id}/comments Verified Create comment (with optional attachments)
DELETE /api/posts/{post_id}/comments/{id} Author/Owner Delete comment and attachments

POST /api/posts/{post_id}/comments

{
  "content": "Great post!",
  "parent_id": null,
  "upload_keys": [],
  "links": []
}

Set parent_id to reply to another comment. Comments are auto-upvoted by the author. Max 4 images OR 1 video, max 4 links.


Identity Resolution

Method Endpoint Auth Description
POST /api/comments/resolve-identities No Resolve anonymous IDs to real identities

POST /api/comments/resolve-identities

{
  "viewer_public_key": "f13CFZY6Yq+RPkY6...",
  "anon_ids": ["7X3K9M2P", "A1B2C3D4"]
}

Returns:

{
  "identities": {
    "7X3K9M2P": "https://activistguy.node.you",
    "A1B2C3D4": null
  }
}

A null value means the commenter has not verified the viewer (or the commenter's node was unreachable).


Votes

Method Endpoint Auth Description
POST /api/votes/{target_type}/{target_id} Verified Vote on a post or comment
DELETE /api/votes/{target_type}/{target_id} Verified Remove vote

target_type is post or comment. Vote value must be 1 or -1. Updating an existing vote is handled automatically.


Uploads

Method Endpoint Auth Description
POST /api/uploads Verified Upload and process a media file
DELETE /api/uploads/{upload_key} Verified Cancel staged upload
GET /api/uploads/temp/{filename} No Preview staged file

POST /api/uploads

Multipart file upload. Returns:

{
  "upload_key": "550e8400-...",
  "media_type": "image",
  "mime_type": "image/jpeg",
  "file_size": 245760,
  "width": 1920,
  "height": 1080,
  "thumbnail_url": "https://bob.node.you/api/uploads/temp/thumb_...",
  "expires_at": "2026-03-16T12:00:00Z"
}

Supported images: JPEG, PNG, GIF, WebP. Supported video: MP4, WebM, MOV (transcoded to H.264/AAC MP4).


Media

Method Endpoint Auth Description
GET /api/media/{filename} No Serve permanent media file

Files are served with Cache-Control: public, max-age=31536000, immutable since they're identified by UUID and never change.


Attachment Limits

Posts Comments
Images 20 4
Videos 1 1
Links 10 4

Images and videos cannot be mixed in the same post or comment.


Schemas

Profile

ProfileUpdate:
    display_name: str | None
    bio: str | None

ProfileResponse:
    display_name: str
    bio: str
    picture_url: str | None
    updated_at: datetime

Post

PostCreate:
    title: str            # 1–300 chars
    body: str | None      # max 40,000 chars
    is_private: bool = False
    status: "draft" | "published"
    upload_keys: list[str]
    links: list[str]

PostResponse:
    id: int
    title: str
    body: str | None
    status: "draft" | "published"
    created_at: datetime
    published_at: datetime | None
    vote_score: int
    is_private: bool

Comment

CommentCreate:
    content: str          # 1–10,000 chars
    parent_id: int | None
    upload_keys: list[str]
    links: list[str]

AnonymizedCommentResponse:
    id: int
    post_id: int
    parent_id: int | None
    anon_id: str          # e.g., "7X3K9M2P"
    content: str
    created_at: datetime
    vote_score: int
    user_vote: int | None
    author_url: str | None  # populated after identity resolution
    attachments: list[AttachmentBrief]

Attachment

AttachmentResponse:
    id: int
    target_type: "post" | "comment"
    target_id: int
    attachment_type: "image" | "video" | "link"
    position: int
    filename: str | None
    mime_type: str | None
    file_size: int | None
    width: int | None
    height: int | None
    media_url: str | None
    thumbnail_url: str | None
    url: str | None        # for link attachments

AttachmentBrief:
    id: int
    attachment_type: "image" | "video" | "link"
    position: int
    media_url: str | None
    thumbnail_url: str | None
    url: str | None
    width: int | None
    height: int | None

Vote

VoteCreate:
    value: 1 | -1

VoteResponse:
    id: int
    target_type: str
    target_id: int
    voter_public_key: str
    voter_url: str
    value: int
    created_at: datetime

Upload

UploadResponse:
    upload_key: str
    media_type: "image" | "video"
    mime_type: str
    file_size: int
    width: int | None
    height: int | None
    thumbnail_url: str
    expires_at: datetime

Auth

SessionRequest:
    public_key: str
    url: str
    timestamp: str
    destination: str
    signature: str

SessionResponse:
    token: str
    expires_in: int       # seconds
    is_owner: bool
    is_verified: bool
    public_key: str
    url: str

Project Structure

app/
├── routers/
│   ├── comments.py          # Comment CRUD (anonymized)
│   ├── feed.py              # Feed with threaded comments
│   ├── identity.py          # Anonymous ID resolution
│   ├── media.py             # Permanent media serving
│   ├── posts.py             # Post CRUD + publish/unpublish
│   ├── profile.py           # Profile info + picture
│   ├── session.py           # Auth sessions + challenge flow
│   ├── uploads.py           # Media upload staging
│   └── votes.py             # Voting on posts/comments
├── schemas/
│   ├── attachment.py
│   ├── comment.py
│   ├── post.py
│   ├── profile.py
│   ├── upload.py
│   └── vote.py
├── crud/                    # Database operations
├── models/                  # SQLAlchemy models
├── services/
│   └── attachments.py       # Upload claiming + file management
├── dependencies/
│   ├── auth.py              # Session management + auth guards
│   └── challenge.py         # In-memory challenge store
├── utils/
│   ├── anonymization.py     # Deterministic anonymous ID generation
│   ├── crypto.py            # Ed25519 signature verification
│   └── media.py             # Image/video processing
├── config.py                # Settings (env-based)
└── database.py              # Async SQLAlchemy (SQLite)

Configuration

Settings are loaded from environment variables (or .env):

Variable Required Description
OWNER_PUBLIC_KEY Yes Ed25519 public key of the profile owner
PROFILE_URL Yes This profile's public URL (e.g., https://bob.node.you)
NODE_API_URL Yes Owner's node API URL for verification lookups
JWT_SECRET Yes Secret for signing session JWTs
DATABASE_PATH No SQLite path (default: profile.db)
JWT_EXPIRY_HOURS No Session duration (default: 24)
CORS_ORIGINS No Allowed origins (default: *)
SIGNATURE_TOLERANCE No Max signature age in seconds (default: 300)
ANON_SALT No Salt for anonymous ID generation
NODE_SUBDOMAIN No Dev-only subdomain override

Tech Stack

Component Technology
Framework FastAPI
Database SQLite (async, via SQLAlchemy)
Migrations Alembic
Auth Ed25519 signatures + JWT sessions
Identity Cryptographic anonymization (SHA-256 + base36)
Media Pillow (images), ffmpeg (video transcoding)
Container Docker (Python 3.12-slim)

License

This project is licensed under the AGPL 3.0.

Appearance
Theme
—