Your Nostr Key Is Your Login

Bridging a million cryptographic identities to Solid pods
April 2026 · melvin.me · Part 2 of Solid Articles

1. Two Worlds, Same Idea

Solid gives you a pod — a personal data server where you control who sees what. Your identity is a WebID: a URL like https://alice.example/profile/card#me that resolves to a profile document.

Nostr is a decentralised social protocol. Over a million people use it. Your identity is a keypair: a private key that signs messages, and a public key that anyone can verify. No server needed for identity — the cryptography is the identity.

Both protocols believe in the same thing: you should own your identity and your data. They just started from different places. Solid started from the web and linked data. Nostr started from cryptography and relay networks.

This article connects them. Your Nostr key can authenticate you to a Solid pod. No signup, no password, no email. If you have a Nostr key, you already have a login.

2. What Is a Nostr Key?

A Nostr key is an secp256k1 keypair — the same elliptic curve Bitcoin uses. It consists of:

WhatFormatExample
Private key64-char hex3f7a...c9d2
Public key64-char hex5e76...45af

The private key signs things. The public key verifies signatures. You never share your private key. Your public key is your identity.

If you don't have a Nostr key yet, generate one:

npx noskey

Save the output. The hex public key (64 characters) is what we'll use.

3. did:nostr — A Key Becomes an Identity

A DID (Decentralised Identifier) is a W3C standard for identities that don't depend on any central authority. The did:nostr method maps a Nostr public key to a DID:

did:nostr:5e7617ea570cade67392eeec16f6666ec942b9215e0276939bf8f8ea869145af

That's it. No resolver, no registry, no blockchain lookup. The public key is the identifier. Anyone who knows the key can verify signatures from it.

In the Solid world, this DID works just like a WebID. You can put it in an .acl file as an acl:agent, and the server will recognise it when you authenticate.

4. NIP-98 — Signing HTTP Requests

HTTP normally uses tokens or cookies for authentication. NIP-98 (also specified as HTTP Schnorr Auth in the W3C Nostr Community Group) does it with cryptographic signatures instead.

To authenticate a request, you sign a Nostr event that says:

FieldValuePurpose
kind27235NIP-98 HTTP Auth event
created_atUnix timestampMust be within ±60 seconds of now
Tag uThe full URLBinds the signature to this specific URL
Tag methodGET, PUT, etc.Binds the signature to this HTTP method
pubkeyYour hex public keyWho is authenticating
sigSchnorr signatureProves you hold the private key

The signed event is base64-encoded and sent in the Authorization header:

Authorization: Nostr <base64-encoded-event>

The server decodes the event, checks the timestamp, verifies the URL and method match the request, validates the Schnorr signature, and — if everything checks out — identifies you as did:nostr:<your-pubkey>.

Why this matters

No secrets are transmitted. Unlike Bearer tokens, the signature proves you hold the private key without ever sending it. Each signature is bound to a specific URL and method, so a captured signature can't be replayed against a different endpoint.

No account needed. The server doesn't need to know about you in advance. Any valid Schnorr signature from any Nostr key is accepted. Your identity is derived from the key, not from a database.

5. Trying It Out

If you followed Part 1, you have JSS running on localhost:4443 with Alice's pod. Let's authenticate with a Nostr key instead of a Bearer token.

First, create a small script that generates a NIP-98 auth header. Save this as nip98.mjs:

import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools';
import { bytesToHex } from '@noble/hashes/utils';

// Use your own key, or generate a new one
const sk = generateSecretKey();
const pk = getPublicKey(sk);

const url = process.argv[2] || 'http://localhost:4443/';
const method = process.argv[3] || 'GET';

const event = finalizeEvent({
  kind: 27235,
  created_at: Math.floor(Date.now() / 1000),
  tags: [['u', url], ['method', method]],
  content: ''
}, sk);

const token = Buffer.from(JSON.stringify(event)).toString('base64');
console.log(`pubkey: ${pk}`);
console.log(`did:nostr:${pk}`);
console.log(`Authorization: Nostr ${token}`);

Install the dependency and run it:

npm install nostr-tools
node nip98.mjs http://localhost:4443/alice/public/note.json GET

This prints your public key and the Authorization header. Now use it:

curl -H "Authorization: Nostr <paste-the-token>" \
  http://localhost:4443/alice/public/note.json

That reads Alice's public note — which was already public, so this just proves the auth header is accepted. The interesting part is using it for private resources.

6. Granting Access to a Nostr Key

Remember the ACL from Part 1? We granted Bob access by his WebID. Now let's grant access by a Nostr public key instead.

Say your Nostr hex pubkey is 5e7617ea570cade67392eeec16f6666ec942b9215e0276939bf8f8ea869145af. Create an ACL that lets that key read Alice's secret note:

curl -X PUT http://localhost:4443/alice/private/secret.json.acl \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/ld+json" \
  -d '[
    {
      "@context": {"acl": "http://www.w3.org/ns/auth/acl#"},
      "@id": "#owner",
      "@type": "acl:Authorization",
      "acl:agent": {"@id": "http://localhost:4443/alice/profile/card#me"},
      "acl:accessTo": {"@id": "http://localhost:4443/alice/private/secret.json"},
      "acl:mode": [{"@id": "acl:Read"}, {"@id": "acl:Write"}, {"@id": "acl:Control"}]
    },
    {
      "@context": {"acl": "http://www.w3.org/ns/auth/acl#"},
      "@id": "#nostr-user",
      "@type": "acl:Authorization",
      "acl:agent": {"@id": "did:nostr:5e7617ea570cade67392eeec16f6666ec942b9215e0276939bf8f8ea869145af"},
      "acl:accessTo": {"@id": "http://localhost:4443/alice/private/secret.json"},
      "acl:mode": [{"@id": "acl:Read"}]
    }
  ]'

Now test it:

# Generate NIP-98 header for the private resource
node nip98.mjs http://localhost:4443/alice/private/secret.json GET

# Use it
curl -H "Authorization: Nostr <paste-the-token>" \
  http://localhost:4443/alice/private/secret.json

If the pubkey in your NIP-98 event matches the did:nostr in the ACL, you get the secret note. If it doesn't match, you get 403.

What just happened

A Nostr key — the same key you use to post on Nostr, sign zaps, and log into apps — just authenticated you to a Solid pod and accessed a private resource. The same ACL system handles WebIDs, Bearer tokens, and Nostr keys. No separate auth system. No schism. One access control layer, multiple identities.

7. How the Pieces Fit Together

ConceptSolidNostrBridge
IdentityWebID (URL)Public key (hex)did:nostr
Auth methodBearer token / Solid-OIDCSchnorr signatureNIP-98 / HTTP Schnorr Auth
Data storagePod (your server)Relays (someone else's server)Use both
Access control.acl files (WAC)Relay-level (coarse)WAC with did:nostr agents
UsersThousands1M+ keysAny Nostr key works in a pod ACL

The bridge is one line in an ACL file. No protocol changes. No new servers. No migration. A Solid pod operator adds a did:nostr to their ACL, and a million existing Nostr users can authenticate.

8. The Specs

This isn't ad-hoc. The bridge is built on three W3C-track specifications from the Nostr W3C Community Group:

SpecWhat It DoesLink
did:nostrMaps Nostr pubkeys to W3C DIDsnostrcg.github.io/did-nostr
HTTP Schnorr AuthNIP-98 as a W3C spec for HTTP authnostrcg.github.io/http-schnorr-auth
Web Access ControlFine-grained permissions for web resourceswebacl.org

JSS implements all three. The combination means: any Nostr key holder can be granted access to any resource on any Solid pod, using standard ACL files, authenticated by standard Schnorr signatures.