node.you Logo Image node.you - API

The node.you API handles account and node management, subdomain assignment, service agent management, service provisioning and routing across servers, and other features integral to the node.you software system.

Architecture

Clients interact with the API, and the API interacts with their services hosted across multiple servers.

+---------------------------------------------+
|                   Clients                   |
|                (Web / Apps)                 |
+---------------------+-----------------------+
                      |
                      v
+---------------------------------------------+
|            node.you API                     |
|            (FastAPI + PostgreSQL)           |
|                                             |
|  +----------+ +-------+ +------+ +-------+  |
|  | Accounts | | Nodes | | Auth | | Admin |  |
|  +----------+ +-------+ +------+ +-------+  |
|  +----------+ +---------+ +-------------+   |
|  | Profiles | | Billing | |  Relations  |   |
|  +----------+ +---------+ +-------------+   |
+---------------------+-----------------------+
                      |
        +-------------+----------------+
        v             v                v
+--------------+ +--------------+ +--------------+
|  Service     | |  Service     | |  Service     |
|  Server 1    | |  Server 2    | |  Server N    |
|              | |              | |              |
| +----------+ | | +----------+ | | +----------+ |
| |Service   | | | |Service   | | | |Service   | |
| |Container | | | |Container | | | |Container | |
| +----------+ | | +----------+ | | +----------+ |
| +----------+ | | +----------+ | |     ...      |
| |Service   | | | |Service   | | |              |
| |Container | | | |Container | | |              |
| +----------+ | | +----------+ | |              |
+--------------+ +--------------+ +--------------+

Clients connect with the API hosting their account. There is an official API for typical users (https://node.you/api/), or the whole stack can be self-hosted under a custom domain.

Nodes

Users create nodes which are online identities backed by Ed25519 key-pairs. By using cryptography, this provides users with a mechanism to verify other users.

A node is an online identity because it can be reached with the user-chosen subdomain, like user.node.you.

Host-Custody Key Storage

For host-custody nodes the API generates the keypair and holds the private key. Private keys are encrypted at rest with Fernet using the KEY_ENCRYPTION_KEYS master key before they touch the database, so a leaked database dump or backup does not expose key material (self-custody nodes never send the API a private key at all).

KEY_ENCRYPTION_KEYS is required and comma-separated: the first key encrypts, every listed key can decrypt. To rotate, prepend a new key, restart, run python -m scripts.rotate_key_encryption, then remove the old key and restart again. When upgrading an existing deployment, set the variable and run alembic upgrade head to encrypt previously stored keys.

Verification

Users verify other users that they know are real people. By ensuring this, users keep bots out of their network.

To verify someone, scan their node's QR code. This leads to the /code endpoint which includes their public key in the response. The key is stored on a relation: one contact row per known node, carrying two independent flags, verified (I know this is a real person) and following (their posts join my feed).

           Example: Hunter verifies Hanna

+--------------+                    +----------------+
|    Hunter    |                    |     Hanna      |
|              |                    |                |
|     /=\      |                    |  +----------+  |
| +----------+ |------------------>>|  |[ ].' .[ ]|  |
| |   / ~\   | | Hunter scans       |  |:.':.'.':.|  |
| |   \__/   | | Hanna's QR code.   |  |': ':.': .|  |
| +----------+ |                    |  |[ ].':.-.'|  |
|              |                    |  +----------+  |
|              |                    |    | /code     |
| +----------+ |                    |    V           |
| | Verified | |<<------------------| +------------+ |
| | - ...    | | Hunter stores      | | publicKey: | |
| | - 6Arh.. | | Hanna's key in his | | 6ArhRe5N.. | |
| +----------+ | relations.         | +------------+ |
+--------------+                    +----------------+

It's important to note that verification is a one way relationship. In the above scenario, Hunter verifies Hanna, but Hanna would need to scan Hunter's code to verify him.

Authentication Flow

There are different forms of authentication depending on whether you are logging into your account or into a service.

Account Login

Clients authenticate their account by passing their credentials to the API's /api/auth/token endpoint. If valid, the API returns new session tokens.

Client                               API
  |                                   | 
  |  POST:                            | 
  |  /api/auth/token                  | 
  |  (email + password)               | 
  |--------------------------------->>| 
  |<<---------------------------------| 
  |  { access_token, refresh_token }  | 
  |                                   | 

This logs the user into their account where they can access their nodes and activate new services.

Service Login

Logging into services is more complex, but for a good reason.

Services operate independently from this API. This means that users cannot use their account credentials to log into services.

Plus, since services are self-hostable, we can't guarantee the security of user-entered credentials going to those external services.

To solve these issues, services have been designed to not require user-entered credentials at all. Instead, a user produces a valid signature with their node that serves as a one-time-password to a designated service.

Users can copy-and-paste their generated signature into the service's login dialog. Client apps automate this: they obtain the signature from /api/sign and submit it to the service themselves, so the only login users deal with is their account login.

The client producing the signature, the authorizer, does not have to be the same client trying to log into the service, the claimant. This is useful for situations like using a phone to authorize a service login on a desktop's web browser.

Client (Claimant)                               Service
  |                                                 |
  |-----CLAIMANT ASKS SERVICE FOR LOGIN CODE------>>|
  |                                                 |
  |                                                 |
  | Client (Authorizer)                             |
  |   |                                             |
  |   |<<----SERVICE PRESENTS CODE FOR AUTHORIZER---|
  |   |                                             |
  |   |                                             |
  |   |                     API                     |
  |   |                      |                      |
  |   | POST:                |                      |
  |   | /api/sign-and-submit |                      |
  |   | (ask API to sign)    |                      |
  |   |-------------------->>|                      |
  |   |                      | (send signature)     |
  |   |                      |-------------------->>|
  |   |                      |<<--------------------|
  |   |<<--------------------| {status:"submitted"} |
  |   |                      |                      |
  |                                                 |
  |                                                 |
  |<<----SERVICE SENDS CLAIMANT AN ACCESS TOKEN-----|
  |                                                 |

The /api/sign-and-submit variant shown above (the API POSTing the signature to the service's callback on the client's behalf) is deprecated and slated for removal; the current client signs with /api/sign and submits the signature itself.

API Reference

Accounts

Method Endpoint Auth Description
POST /api/accounts No Register a new account
GET /api/accounts/me Yes Get current account info

POST /api/accounts

{
  "email": "user@example.com",
  "password": "...",
  "accept_terms": true
}

Returns 201 with account details. Requires accept_terms: true.

When the deployment runs REGISTRATION_MODE=invite, a valid single-use invite_code is also required (closed refuses self-registration entirely; emails in ADMINS bypass gating either way). The deployment's default service allowance, or the invite code's own, seeds the new account's entitlement.

GET /api/accounts/me

Returns the authenticated account. Requires Bearer token.


Auth

Method Endpoint Auth Description
POST /api/auth/token No Login (email + password)
POST /api/auth/refresh No Rotate token pair

POST /api/auth/token

Form-encoded (OAuth2PasswordRequestForm):

Field Description
username Email address
password Account password

Returns:

{
  "access_token": "eyJ...",
  "refresh_token": "eyJ...",
  "token_type": "bearer"
}

POST /api/auth/refresh

{
  "refresh_token": "eyJ..."
}

Returns a new token pair. Implements refresh token rotation: each refresh token is single-use.


Nodes

Method Endpoint Auth Description
GET /api/nodes/check/{subdomain} No Check subdomain availability
POST /api/nodes Yes Register a new node
GET /api/nodes Yes List your nodes

POST /api/nodes

{
  "subdomain": "activistguy",
  "label": "Local activism"
}

Returns 201. Generates an Ed25519 key-pair on creation. The subdomain must be unique and pass format validation.

GET /api/nodes/check/{subdomain}

{
  "subdomain": "activistguy",
  "available": true,
  "reason": null
}

Relations

One row per contact, carrying the two independent flags: verified and following.

Method Endpoint Auth Description
POST /api/relations Yes Create or upsert a relation
GET /api/relations/{subdomain} Yes List a node's relations
GET /api/relations/{subdomain}/{id} Yes Get one relation
PATCH /api/relations/{subdomain}/{id} Yes Update label or flags
DELETE /api/relations/{subdomain}/{id} Yes Delete the relation

POST /api/relations

{
  "owner_subdomain": "activistguy",
  "public_key": "lwPu2Jbp9DL5lD1F...",
  "node_url": "https://striker.getreadytostrike.org",
  "label": "Striker",
  "verified": true,
  "following": false
}

Upserts: re-adding an existing contact turns on any newly true flags (existing true flags are preserved) and refreshes node_url. Clearing a relation's last remaining flag deletes the row (the PATCH returns 204).


Node Public Endpoints

These are served on {subdomain}.node.you via subdomain routing:

Method Endpoint Auth Description
GET / No Redirect to /code
GET /code No Get node's public key + profile status
GET /api/verifications/check?public_key=... No Check if a public key is verified by this node
GET /api/followings/check?public_key=... No Check if a public key is followed by this node

GET /code

{
  "public_key": "lwPu2Jbp9DL5lD1F...",
  "has_profile": true
}

GET /api/verifications/check

Used by profile services to verify identity:

{
  "verified": true,
  "public_key": "lwPu2Jbp9DL5lD1F...",
  "seen_first": "https://striker.getreadytostrike.org",
  "seen_last": "",
  "verified_at": "2026-02-15T12:00:00"
}

Node Private Endpoints

These require authentication and are used for cryptographic signing:

Method Endpoint Auth Description
POST /api/sign Yes Sign a message with node's private key
POST /api/sign-and-submit Yes (Deprecated) Sign and POST to service callback

POST /api/sign

Query Param Description
destination Service URL being authenticated to
node_subdomain Node performing the signing

Returns:

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

The signed message format is {timestamp}:{destination}.

POST /api/sign-and-submit

Deprecated (unused by the current client; slated for removal). Same as /api/sign but adds a callback_url param and POSTs the signed payload directly to the service's challenge endpoint. Returns 502 if the service rejects the signature.

The callback_url must be https and resolve to a public address (the API validates it before signing, so it can't be pointed at private/internal hosts or cloud metadata). TLS is verified, redirects are not followed, and the service's response body is never echoed back in the error.


Profiles (Admin)

Method Endpoint Auth Description
POST /api/admin/profiles Admin Provision a new profile container
DELETE /api/admin/profiles/{subdomain} Admin Teardown profile (preserves DB)
GET /api/admin/profiles/{subdomain} Admin Get profile status
GET /api/admin/profiles Admin List all profiles for account

POST /api/admin/profiles

{
  "subdomain": "alice"
}

Provisions a Docker container and nginx config on an available service server. Returns profile details including assigned port and status.

Profile Status Values

Status Meaning
ACTIVE Container running, nginx configured
STOPPED Container stopped
ERROR Provisioning failed (check error_message)

Server Config

Method Endpoint Auth Description
GET /api/server/config No Public deployment discovery

Reports the registration mode, default service allowance, enabled billing providers, the Stripe publishable key, and the operator's display price for a new service (SERVICE_PRICE_DISPLAY, suppressed when no billing is mounted). Clients probe this one endpoint to adapt (invite field, checkout vs. activate, price badge) instead of interpreting provider-route 404s.


Services on Nodes

Method Endpoint Auth Description
POST /api/nodes/{subdomain}/profile Yes Activate a profile against a granted allowance
DELETE /api/nodes/{subdomain}/profile Yes Deactivate the profile (billing adjusted first; data preserved)

Activation checks the account's granted allowance (provider "none") and provisions inline. Billing-managed accounts are refused (409); their services activate through checkout instead.


Admin

All /api/admin/* endpoints require an account whose email is in ADMINS. The client app fronts these in its Admin pane; they are also usable directly (see the Swagger UI at /api/docs).

Endpoints Description
POST/GET /api/admin/invites, DELETE /api/admin/invites/{code} Generate, list, and revoke single-use registration invites (optional per-code allowance)
GET /api/admin/accounts, PUT /api/admin/accounts/{id}/allowance Account roster with entitlement state; set or clear granted allowances
GET/POST /api/admin/servers, PUT /api/admin/servers/{id} Register and manage service servers (address, capacity, status)
/api/admin/profiles... Provision, tear down, and inspect profiles (tables above)
/api/admin/reports... Review reports submitted via POST /api/reports

Stripe Integration

Mounted only when Stripe credentials are configured; a deployment with no billing provider runs unmetered. /api/server/config reports which providers are live.

Method Endpoint Auth Description
GET /api/stripe/config No Publishable key for this provider
GET /api/stripe/preview-checkout?subdomain=... Yes Preview what a checkout would do (path + exact prorated amount), charging nothing
POST /api/stripe/create-checkout Yes Create checkout session for new profile
POST /api/stripe/webhook No Handle Stripe events
POST /api/stripe/create-portal-session Yes Open Stripe billing portal

GET /api/stripe/config

Returns { "publishable_key": "pk_..." } so clients never hardcode the key: each host (prod, dev, or a self-hosted provider) serves its own from its .env. The publishable key is not secret, so this endpoint is unauthenticated. An empty string means the provider hasn't configured Stripe.

POST /api/stripe/create-checkout?subdomain=alice

If the account has no existing subscription, creates a Stripe Checkout Session. If the account already has a subscription, updates the subscription quantity with proration.

Webhook Events

Event Action
checkout.session.completed Saves subscription ID, provisions first profile
invoice.paid Provisions any pending profile from subscription metadata
customer.subscription.updated Logged (quantity change tracking)
customer.subscription.deleted Tears down all profiles, clears subscription

Google Play Integration

/api/google-play/* (purchase verification, RTDN webhook, Play-rail deactivation) mounts only when a service-account path is configured. Play billing is bound to the official app's Play Console listing and does not transfer to self-hosted deployments; self-hosted providers use Stripe.


Schemas

Account

AccountCreate:
    email: EmailStr
    password: str
    accept_terms: bool

AccountResponse:
    id: int
    email: str
    created_at: datetime
    tos_accepted_at: datetime | None
    is_admin: bool
    # plus a subscription summary (provider, status, service_count, ...)

Node

NodeCreate:
    subdomain: str
    label: str = ""

NodeResponse:
    id: int
    subdomain: str
    label: str
    public_key: str
    profile_url: str | None
    created_at: datetime

Relation

RelationCreate:
    owner_subdomain: str
    public_key: str
    node_url: str
    label: str = ""
    verified: bool = True
    following: bool = True

RelationResponse:
    id: int
    public_key: str
    node_url: str
    label: str
    verified: bool
    following: bool
    verified_at: datetime | None
    followed_at: datetime | None
    created_at: datetime
    updated_at: datetime

Profile

ProfileProvision:
    subdomain: str

ProfileResponse:
    id: int
    node_id: int
    subdomain: str
    port: int
    status: ProfileStatus  # ACTIVE | STOPPED | ERROR
    error_message: str | None
    created_at: datetime
    updated_at: datetime

Auth

Token:
    access_token: str
    refresh_token: str
    token_type: str = "bearer"

RefreshRequest:
    refresh_token: str

Project Structure

app/
├── routers/
│   ├── accounts.py          # Registration (invite gating) + /me
│   ├── auth.py              # Login, token refresh
│   ├── nodes.py             # Node CRUD + subdomain check + self-serve activation
│   ├── relations.py         # Contacts: verified + following flags
│   ├── node_public.py       # Subdomain-routed public endpoints
│   ├── node_private.py      # Signing endpoints
│   ├── server_config.py     # Public deployment discovery
│   ├── reports.py           # User content reports
│   ├── stripe.py            # Checkout, webhooks, portal (mounted when configured)
│   ├── google_play.py       # Play verification + RTDN webhook (official app only)
│   ├── admin_profiles.py    # Profile provisioning (admin)
│   ├── admin_accounts.py    # Account roster + allowance grants (admin)
│   ├── admin_invites.py     # Invite codes (admin)
│   ├── admin_servers.py     # Service-server management (admin)
│   └── admin_reports.py     # Report review (admin)
├── schemas/                 # Pydantic request/response models
├── crud/                    # Database operations
├── models/                  # SQLAlchemy models
├── services/
│   ├── activation.py        # Provider-neutral profile activation
│   ├── billing.py           # Provider dispatch on deactivation
│   ├── provisioning.py      # Service-agent calls + nginx provisioning
│   ├── stripe_billing.py    # Stripe subscription adjustments
│   ├── google_play.py       # Play Developer API calls
│   └── usage.py             # Profile storage-usage pipeline
├── dependencies/
│   ├── auth.py              # Auth dependencies + token utils
│   └── pubsub_oidc.py       # Play RTDN push authentication
├── utils/
│   ├── crypto.py            # Ed25519 signing utilities
│   ├── keycrypt.py          # At-rest encryption for host-custody keys
│   ├── security.py          # Password hashing (bcrypt)
│   ├── content_negotiation.py  # HTML vs JSON preference for node pages
│   └── ssrf.py              # Outbound callback URL validation (SSRF guard)
├── config.py                # Settings (env-based)
└── database.py              # Async SQLAlchemy session

deploy/                      # Setup scripts, systemd unit, nginx template
guides/                      # Self-hosting guides (doc site topics)
migrations/                  # Alembic migration chain
scripts/                     # Operational one-shots (nginx rewrite, key rotation)

Tech Stack

Component Technology
Framework FastAPI
Database PostgreSQL (async, via SQLAlchemy)
Profile DBs SQLite (per-container)
Migrations Alembic
Auth JWT (access + refresh tokens)
Identity Ed25519 keypairs
Key storage Fernet encryption at rest (KEY_ENCRYPTION_KEYS)
Containers Docker (per-profile)
Payments Stripe (subscriptions, quantity-based); Google Play on the official app
Reverse Proxy nginx (per-profile subdomain config)

Self-Hosting

The whole stack is designed to run under your own domain, with or without billing. Start with the Self-Hosting Overview; the deploy/ directory in this repository carries the setup scripts, systemd unit, and nginx template those guides walk through.

License

This project is licensed under the AGPL 3.0.

Appearance
Theme
—