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.
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.
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.
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.
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.
There are different forms of authentication depending on whether you are logging into your account or into a service.
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.
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-submitvariant 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/signand submits the signature itself.
| 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/meReturns the authenticated account. Requires Bearer token.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/api/auth/token |
No | Login (email + password) |
POST |
/api/auth/refresh |
No | Rotate token pair |
POST /api/auth/tokenForm-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.
| 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
}
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).
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/checkUsed 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"
}
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-submitDeprecated (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.
| 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.
| Status | Meaning |
|---|---|
ACTIVE |
Container running, nginx configured |
STOPPED |
Container stopped |
ERROR |
Provisioning failed (check error_message) |
| 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.
| 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.
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 |
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/configReturns { "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=aliceIf the account has no existing subscription, creates a Stripe Checkout Session. If the account already has a subscription, updates the subscription quantity with proration.
| 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 |
/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.
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, ...)
NodeCreate:
subdomain: str
label: str = ""
NodeResponse:
id: int
subdomain: str
label: str
public_key: str
profile_url: str | None
created_at: datetime
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
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
Token:
access_token: str
refresh_token: str
token_type: str = "bearer"
RefreshRequest:
refresh_token: str
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)
| 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) |
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.
This project is licensed under the AGPL 3.0.