-
Notifications
You must be signed in to change notification settings - Fork 0
Home
Demo: https://nostr.app/
Nostr.App is a collection of web applications built on the Nostr protocol, designed to provide users with various utilities while leveraging the decentralized nature of Nostr for authentication and data storage. This document provides a detailed analysis of the system architecture, frameworks used, and design patterns implemented across the application suite.
The system consists of multiple standalone web applications that share common components, authentication mechanisms, and storage solutions. Each application serves a specific purpose while maintaining a consistent user experience and leveraging the same underlying infrastructure.
Nostr.App is built around the Nostr protocol (Notes and Other Stuff Transmitted by Relays), a simple, open protocol that enables censorship-resistant and decentralized social media. The applications in this suite leverage Nostr for:
- Authentication: Users can log in using Nostr extensions or private keys
- Data Storage: User data is stored using nosdav.net, a Nostr-based storage solution
- Identity Management: User identities are managed through Nostr public/private key pairs
The system includes several applications:
- Todo App: A task management application for creating and tracking to-do items
- Bookmark Manager: An application for saving and organizing web bookmarks
- Pastebin: A tool for sharing code and text snippets
- Key Generator: A utility for creating new Nostr identity key pairs
- Profile Manager: A tool for managing user profiles
- Hello World: A simple starter application
The system follows a client-side architecture where all processing happens in the browser. The applications are built as Single Page Applications (SPAs) using modern web technologies. The architecture can be broken down into:
- Presentation Layer: UI components built with Preact and styled with CSS/Tailwind
- Application Logic Layer: JavaScript modules handling business logic
- Data Layer: Storage mechanisms using nosdav.net and localStorage as fallback
- Authentication Layer: Nostr-based authentication using extensions or private keys
The system uses Preact, a lightweight alternative to React, for building user interfaces. Preact offers:
- Virtual DOM implementation for efficient rendering
- Component-based architecture
- Small footprint (3KB) compared to React
- API compatibility with React
The applications use HTM (Hyperscript Tagged Markup), a JSX alternative that uses tagged template literals. This allows JSX-like syntax without requiring a build step:
const html = htm.bind(h)
const component = html`<div>Hello ${name}</div>`The applications use a combination of:
-
Custom CSS Variables: Defined in
:rootfor consistent theming across applications - Tailwind CSS: Used in some applications for utility-first styling
- Inline Styles: Used for dynamic styling and overrides
Key external libraries include:
- secp256k1: For cryptographic operations related to Nostr keys
- bech32: For encoding/decoding Nostr keys in human-readable format
- SweetAlert2: For enhanced dialog boxes and notifications
The system uses a tiered storage approach:
- Primary Storage: nosdav.net, a Nostr-based storage solution
- Fallback Storage: Browser's localStorage for offline capability
The applications use ES modules loaded directly from CDNs:
import {
h,
render,
Component
} from 'https://unpkg.com/preact@10.13.1/dist/preact.module.js'
import htm from 'https://unpkg.com/htm@3.1.1/dist/htm.module.js'This approach eliminates the need for bundlers like Webpack or Rollup, allowing the applications to run directly in modern browsers without a build step.
The system follows a component-based architecture where UI elements are broken down into reusable components. The main components include:
- App Components: Top-level components that manage state and orchestrate child components
- Navbar Component: A shared navigation component used across applications
- UI Components: Smaller, reusable components like cards, buttons, and form elements
State management is handled through Preact's built-in state system:
-
Component State: Local state managed within components using
this.stateandthis.setState() - Props: Data passed down from parent to child components
- Lifting State Up: State is lifted to common ancestors when needed by multiple components
The system does not use external state management libraries like Redux or MobX, keeping the architecture simple and lightweight.
The applications use a simple approach to routing:
- Multi-page Approach: Each application is a separate HTML file
-
Anchor Links: Navigation between applications is handled with standard
<a>tags - No Client-side Router: Unlike many modern SPAs, there's no client-side router like React Router
Data fetching is handled through the Fetch API with custom wrappers:
- nosdav-shim.js: Intercepts fetch requests to add Nostr authentication headers
- Storage Providers: Abstract the details of data fetching and storage
The authentication system supports multiple methods:
- Nostr Extension: Users can authenticate using browser extensions like nos2x or Alby
- Private Key: Users can directly enter their Nostr private key
- URL Hash: Private keys can be passed via URL hash for quick login
The system extensively uses the Component pattern, breaking the UI into reusable, encapsulated pieces. This promotes:
- Reusability: Components can be used across different parts of the application
- Maintainability: Changes to a component are reflected everywhere it's used
- Separation of Concerns: Each component handles a specific part of the UI
Example from the Navbar component:
class Navbar extends Component {
constructor(props) {
super(props)
this.state = {
isLoggedIn: localStorage.getItem('loggedIn') === 'true',
pubkey: localStorage.getItem('pubkey') || null,
privkey: localStorage.getItem('nostr:privkey') || null
}
}
// Component methods...
render() {
// Render UI based on component state
}
}The system uses the Strategy pattern for storage mechanisms. This allows the application to switch between different storage strategies (nosdav.net or localStorage) without changing the client code:
getStorageProvider() {
// Return nosdav strategy or localStorage strategy
return {
type: 'nosdav',
isPrimary: true,
save: async data => {
// Implementation details...
},
load: async () => {
// Implementation details...
}
};
}The system uses a simple Factory pattern to create storage providers:
getStorageProvider() {
// Logic to determine which storage provider to use
console.log('Using nosdav.net as primary storage');
return {
// Return the appropriate storage provider object
};
}The nosdav-shim.js file implements the Adapter pattern by wrapping the standard Fetch API and adapting it to work with Nostr authentication:
// Original fetch is stored
const originalFetch = window.fetch
// New fetch adapts the original to add Nostr authentication
window.fetch = async function (url, options) {
const newOptions = { ...options }
// Add Nostr authentication headers if needed
if (newOptions.method === 'PUT') {
// Authentication logic...
newOptions.headers = {
...newOptions.headers,
authorization: auth
}
}
// Call the original fetch with modified options
return originalFetch.call(this, url, newOptions)
}The system implements a simplified version of the Observer pattern through event listeners and state updates:
- Event Listeners: Components listen for user interactions
- State Updates: When state changes, the UI is automatically updated
- Props as Callbacks: Parent components pass callback functions to child components
Example from the TodoApp component:
toggleTodo = id => {
const newTodos = this.state.todos.map(todo =>
todo['@id'] === id ? { ...todo, completed: !todo.completed } : todo
)
this.setState({ todos: newTodos })
this.saveTodos(newTodos)
}The nosdav-shim.js implements a form of the Proxy pattern by intercepting fetch calls and adding authentication:
window.fetch = async function (url, options) {
// This is a proxy that intercepts calls to the original fetch
// and adds authentication headers before forwarding the call
}The storage provider implementation acts as a Facade, providing a simplified interface to the complex storage mechanisms:
// Facade that hides the complexity of storage operations
async initializeStorage() {
const storage = this.getStorageProvider();
const todos = await storage.load();
// Process the loaded data
this.setState({
todos: updatedTodos,
storageType: storage.type
});
}
async saveTodos(todos) {
const storage = this.getStorageProvider();
await storage.save(todos);
}The UI structure follows the Composite pattern, where components can contain other components, creating a tree-like structure:
render() {
return html`
<div>
<${Header} user=${user} onLogin=${this.handleLogin} onLogout=${this.handleLogout} />
<${Sidebar} items=${sidebarItems} />
<${MainContent} popularApps=${popularApps} utilityApps=${utilityApps} />
</div>
`;
}The applications implement a form of the Template Method pattern in their component lifecycle:
class TodoApp extends Component {
constructor() {
// Initialize state
}
componentDidMount() {
// Setup after component is mounted
this.initializeStorage()
}
// Other methods
render() {
// Render the UI
}
}The applications follow a unidirectional data flow pattern:
- State: The application state is stored in component state
- Events: User interactions trigger event handlers
- State Updates: Event handlers update the state
- Re-render: State changes cause components to re-render
This pattern makes the application behavior predictable and easier to debug.
The applications use prop drilling to pass data and callbacks down the component tree:
<${Header}
user=${user}
onLogin=${this.handleLogin}
onLogout=${this.handleLogout}
/>While this approach works for smaller applications, it can become unwieldy in larger applications. The system could benefit from a more sophisticated state management solution for larger-scale applications.
The applications integrate with localStorage for persistent data storage as a fallback:
// Save to localStorage
localStorage.setItem('todos', JSON.stringify(data))
// Load from localStorage
const data = localStorage.getItem('todos')
if (data) {
return JSON.parse(data)
}The primary storage mechanism is nosdav.net, a Nostr-based storage solution:
const url = `https://nosdav.net/${pubkey}/todos.json`
const response = await fetch(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})The system uses Nostr for authentication, which is based on public-key cryptography:
- Public Key: Serves as the user's identity
- Private Key: Used to sign messages and prove ownership of the public key
The system supports multiple authentication methods:
-
Nostr Extension: Browser extensions that manage Nostr keys
const pubkey = await window.nostr.getPublicKey()
-
Private Key Entry: Direct entry of the private key
const pubkey = secp256k1.utils.bytesToHex( secp256k1.schnorr.getPublicKey(privkey) )
-
URL Hash: Private key passed via URL hash
const hash = window.location.hash.substring(1) if (hash && hash.length === 64 && /^[0-9a-fA-F]+$/.test(hash)) { this.loginWithPrivkey(hash) }
The Key Generator application provides tools for:
-
Generating New Keys: Creating new Nostr identities
const privateKeyBytes = getRandomBytes(32) const publicKeyBytes = secp256k1.schnorr.getPublicKey(privateKeyHex)
-
Importing Existing Keys: Using existing Nostr identities
const publicKeyBytes = secp256k1.schnorr.getPublicKey(privateKeyHex)
-
Key Format Conversion: Converting between hex and bech32 formats
const nsec = encodeBech32('nsec', keyPair.privateKeyBytes) const npub = encodeBech32('npub', keyPair.publicKeyBytes)
The primary storage mechanism is nosdav.net, which provides:
- Cloud Storage: Data is stored on remote servers
- Nostr Authentication: Access control using Nostr keys
- JSON Storage: Data is stored in JSON format
The system includes a fallback to localStorage when nosdav.net is unavailable:
try {
// Try nosdav.net first
// ...
} catch (e) {
// Fall back to localStorage
localStorage.setItem('todos', JSON.stringify(data))
}The storage mechanism is abstracted behind a common interface:
{
type: 'nosdav',
isPrimary: true,
save: async data => { /* ... */ },
load: async () => { /* ... */ }
}This allows the application to switch between storage providers without changing the client code.
The applications share a common design system defined through CSS variables:
:root {
--primary: #4a6fa5;
--primary-dark: #3a5683;
--primary-light: #c5d5e5;
--secondary: #8fb8de;
--accent: #63c0f5;
--text-light: #ffffff;
--text-dark: #334155;
--card-bg: #f0f7ff;
--body-bg: #f8fafc;
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
--radius-sm: 6px;
--radius-md: 12px;
}This ensures visual consistency across applications.
The applications implement responsive design using media queries:
@media (max-width: 768px) {
.sidebar {
width: 0;
overflow: hidden;
}
.main-content {
margin-left: 0;
padding: 25px;
}
.card-grid {
grid-template-columns: 1fr;
}
}The applications use subtle animations and transitions to enhance the user experience:
.todo-item {
transition: all 0.3s ease;
border-radius: var(--radius-sm);
}
.todo-item:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-sm);
background-color: white;
border-color: rgba(99, 192, 245, 0.2);
}The applications provide feedback to users through:
- Toast Notifications: Using SweetAlert2 for success/error messages
- Visual Feedback: Hover and active states for interactive elements
- Loading States: Indication when data is being loaded or saved
The system handles private keys with care:
- No Server Transmission: Private keys are never sent to servers
- Local Storage: Private keys are stored in localStorage (with user consent)
- URL Cleaning: Private keys passed via URL hash are removed from the URL after use
The authentication system uses cryptographic signatures for security:
const signedEvent = await signEventWithPrivkey(event, privkey)
auth = `Nostr ${btoa(JSON.stringify(signedEvent))}`Data security is maintained through:
- Authentication Headers: Requests to nosdav.net include authentication headers
- HTTPS: All communication with servers is over HTTPS
- Minimal Data Collection: Only necessary data is collected and stored
The system uses Preact instead of React for better performance:
- Smaller Size: Preact is only 3KB compared to React's larger footprint
- Faster Rendering: Preact's simpler implementation can be faster in many cases
The applications run directly in the browser without a build step:
- ES Modules: Direct loading of ES modules from CDNs
- HTM: JSX-like syntax without requiring transpilation
- CSS Variables: Dynamic styling without preprocessors
Some resources are loaded only when needed:
// Load data only when component mounts
componentDidMount() {
this.loadBookmarks();
}The system's modular architecture allows for easy extension:
- Component Reuse: Components like Navbar are reused across applications
- Shared Utilities: Authentication and storage mechanisms are shared
- Independent Applications: Each application can evolve independently
New applications can be added by:
- Creating a new HTML file
- Importing the shared components and utilities
- Implementing the application-specific logic
Existing applications can be extended by:
- Adding new components
- Enhancing the data model
- Adding new features to the UI
While the codebase doesn't include explicit testing code, the architecture supports testability through:
- Component Isolation: Components can be tested in isolation
- Pure Functions: Many utility functions are pure and easily testable
- State Predictability: Unidirectional data flow makes state changes predictable
The applications are designed for simple deployment:
- Static Files: All files are static HTML, CSS, and JavaScript
- No Server Dependencies: No server-side code is required
- CDN Usage: External libraries are loaded from CDNs
This allows the applications to be deployed on any static file hosting service.
As the applications grow, a more sophisticated state management solution could be beneficial:
- Context API: Preact's Context API could reduce prop drilling
- State Management Library: A lightweight state management library could be added
Adding a build process could provide benefits:
- Code Minification: Reducing file sizes for faster loading
- Tree Shaking: Removing unused code
- Asset Optimization: Optimizing images and other assets
Adding a testing infrastructure would improve reliability:
- Unit Tests: Testing individual components and functions
- Integration Tests: Testing component interactions
- End-to-End Tests: Testing complete user flows
Nostr.App represents a modern approach to web application development, leveraging the Nostr protocol for authentication and data storage. The system's architecture balances simplicity and functionality, providing a solid foundation for building decentralized web applications.
Key strengths of the system include:
- Decentralized Authentication: Using Nostr for user identity
- Flexible Storage: Supporting both cloud and local storage
- Component Reusability: Sharing components across applications
- Modern Web Technologies: Using ES modules, Preact, and HTM
- No Build Step: Running directly in the browser
The system demonstrates how modern web applications can be built without complex tooling while still providing a rich user experience and leveraging decentralized technologies.