Applesauce 0 to 1
- 1. Setup new project
- 2. Adding nostr dependencies
- 3. Setup rx-nostr
- 4. Setup the event store
- 5. Create the query store
- 6. Setup the profile loader
- 7. Fetch fiatjaf’s profile
- 8. Display the profile
- 9. Letting the user signin
- 10. Showing the signed-in state
- 11. Fetching the user’s profile
- 12. Showing if the user is following fiatjaf
- 13. Adding the follow button
- 14. Adding outbox support
How to create a nostr app quickly using applesauce
In this guide we are going to build a nostr app that lets users follow and unfollow @fiatjaf
1. Setup new project
Start by setting up a new vite app using pnpm create vite, then set the name and select Solid and Typescript
➜ pnpm create vite
│
◇ Project name:
│ followjaf
│
◇ Select a framework:
│ Solid
│
◇ Select a variant:
│ TypeScript
│
◇ Scaffolding project in ./followjaf...
│
└ Done. Now run:
cd followjaf
pnpm install
pnpm run dev
2. Adding nostr dependencies
There are a few useful nostr dependencies we are going to need. nostr-tools for the types and small methods, and rx-nostr for making relay connections
pnpm install nostr-tools rx-nostr
3. Setup rx-nostr
Next we need to setup rxNostr so we can make connections to relays. create a new src/nostr.ts file with
import { createRxNostr, noopVerifier } from "rx-nostr";
export const rxNostr = createRxNostr({
// skip verification here because we are going to verify events at the event store
skipVerify: true,
verifier: noopVerifier,
});
4. Setup the event store
Now that we have a way to connect to relays, we need a place to store events. We will use the EventStore class from applesauce-core for this. create a new src/stores.ts file with
The event store does not store any events in the browsers local storage or anywhere else. It’s in-memory only and provides a model for the UI
import { EventStore } from "applesauce-core";
import { verifyEvent } from "nostr-tools";
export const eventStore = new EventStore();
// verify the events when they are added to the store
eventStore.verifyEvent = verifyEvent;
5. Create the query store
The event store is where we store all the events, but we need a way for the UI to query them. We can use the QueryStore class from applesauce-core for this.
Create a query store in src/stores.ts
import { QueryStore } from "applesauce-core";
// ...
// the query store needs the event store to subscribe to it
export const queryStore = new QueryStore(eventStore);
6. Setup the profile loader
Next we need a way to fetch user profiles. We are going to use the ReplaceableLoader class from applesauce-loaders for this.
applesauce-loadersis a package that contains a few loader classes that can be used to fetch different types of data from relays.
First install the package
pnpm install applesauce-loaders
Then create a src/loaders.ts file with
import { ReplaceableLoader } from "applesauce-loaders";
import { rxNostr } from "./nostr";
import { eventStore } from "./stores";
export const replaceableLoader = new ReplaceableLoader(rxNostr);
// Start the loader and send any events to the event store
replaceableLoader.subscribe((packet) => {
eventStore.add(packet.event, packet.from);
});
7. Fetch fiatjaf’s profile
Now that we have a way to store events, and a loader to help with fetching them, we should update the src/App.tsx component to fetch the profile.
We can do this by calling the next method on the loader and passing a pubkey, kind and relays to it
function App() {
// ...
onMount(() => {
// fetch fiatjaf's profile on load
replaceableLoader.next({
pubkey: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
kind: 0,
relays: ["wss://pyramid.fiatjaf.com/"],
});
});
// ...
}
8. Display the profile
Now that we have a way to fetch the profile, we need to display it in the UI.
We can do this by using the ProfileQuery which gives us a stream of updates to a pubkey’s profile.
Create the profile using queryStore.createQuery and pass in the ProfileQuery and the pubkey.
const fiatjaf = queryStore.createQuery(
ProfileQuery,
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"
);
But this just gives us an observable, we need to subscribe to it to get the profile.
Luckily SolidJS profiles a simple from method to subscribe to any observable.
To make things reactive SolidJS uses accessors, so to get the profile we need to call
fiatjaf()
function App() {
// ...
// Subscribe to fiatjaf's profile from the query store
const fiatjaf = from(
queryStore.createQuery(ProfileQuery, "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d")
);
return (
<>
{/* replace the vite and solid logos with the profile picture */}
<div>
<img src={fiatjaf()?.picture} class="logo" />
</div>
<h1>{fiatjaf()?.name}</h1>
{/* ... */}
</>
);
}
9. Letting the user signin
Now we should let the user signin to the app. We can do this by creating a AccountManager class from applesauce-accounts
First we need to install the packages
pnpm install applesauce-accounts applesauce-signers
Then create a new src/accounts.ts file with
import { AccountManager } from "applesauce-accounts";
import { registerCommonAccountTypes } from "applesauce-accounts/accounts";
// create an account manager instance
export const accounts = new AccountManager();
// Adds the common account types to the manager
registerCommonAccountTypes(accounts);
Next lets presume the user has a NIP-07 browser extension installed and add a signin button.
function App() {
const signin = async () => {
// do nothing if the user is already signed in
if (accounts.active) return;
// create a new nip-07 signer and try to get the pubkey
const signer = new ExtensionSigner();
const pubkey = await signer.getPublicKey();
// create a new extension account, add it, and make it the active account
const account = new ExtensionAccount(pubkey, signer);
accounts.addAccount(account);
accounts.setActive(account);
};
return (
<>
{/* ... */}
<div class="card">
<p>Are you following the fiatjaf? the creator of "The nostr"</p>
<button onClick={signin}>Check</button>
</div>
</>
);
}
Now when the user clicks the button the app will ask for the users pubkey, then do nothing… but it’s a start.
We are not persisting the accounts, so when the page reloads the user will NOT be signed in. you can learn about persisting the accounts in the docs
10. Showing the signed-in state
We should show some indication to the user that they are signed in. We can do this by modifying the signin button if the user is signed in and giving them a way to sign-out
function App() {
// subscribe to the currently active account (make sure to use the account$ observable)
const account = from(accounts.active$);
// ...
const signout = () => {
// do nothing if the user is not signed in
if (!accounts.active) return;
// signout the user
const account = accounts.active;
accounts.removeAccount(account);
accounts.clearActive();
};
return (
<>
{/* ... */}
<div class="card">
<p>Are you following the fiatjaf? ( creator of "The nostr" )</p>
{account() === undefined ? <button onClick={signin}>Check</button> : <button onClick={signout}>Signout</button>}
</div>
</>
);
}
11. Fetching the user’s profile
Now that we have a way to sign in and out of the app, we should fetch the user’s profile when they sign in.
function App() {
// ...
// fetch the user's profile when they sign in
createEffect(async () => {
const active = account();
if (active) {
// get the user's relays or fallback to some default relays
const usersRelays = await active.getRelays?.();
const relays = usersRelays ? Object.keys(usersRelays) : ["wss://relay.damus.io", "wss://nos.lol"];
// tell the loader to fetch the users profile event
replaceableLoader.next({
pubkey: active.pubkey,
kind: 0,
relays,
});
// tell the loader to fetch the users contacts
replaceableLoader.next({
pubkey: active.pubkey,
kind: 3,
relays,
});
// tell the loader to fetch the users mailboxes
replaceableLoader.next({
pubkey: active.pubkey,
kind: 10002,
relays,
});
}
});
// ...
}
Next we need to subscribe to the users profile, to do this we can use some rxjs operators to chain the observables together.
import { Match, Switch } from "solid-js";
import { of, switchMap } from "rxjs";
function App() {
// ...
// subscribe to the active account, then subscribe to the users profile or undefined
const profile = from(
accounts.active$.pipe(
switchMap((account) => (account ? queryStore.createQuery(ProfileQuery, account!.pubkey) : of(undefined)))
)
);
// ...
return (
<>
{/* ... */}
<div class="card">
<Switch>
<Match when={account() && !profile()}>
<p>Loading profile...</p>
</Match>
<Match when={profile()}>
<p style="font-size: 1.2rem; font-weight: bold;">Welcome {profile()?.name}</p>
</Match>
</Switch>
{/* ... */}
</div>
</>
);
}
12. Showing if the user is following fiatjaf
Now that the app is fetching the users profile and contacts we should show if the user is following fiatjaf.
function App() {
// ...
// subscribe to the active account, then subscribe to the users contacts or undefined
const contacts = from(
accounts.active$.pipe(
switchMap((account) => (account ? queryStore.createQuery(UserContactsQuery, account!.pubkey) : of(undefined)))
)
);
const isFollowing = createMemo(() => {
return contacts()?.some((c) => c.pubkey === "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d");
});
// ...
return (
<>
{/* ... */}
<div class="card">
{/* ... */}
<Switch
fallback={
<p style="font-size: 1.2rem;">
Sign in to check if you are a follower of the fiatjaf ( creator of "The nostr" )
</p>
}
>
<Match when={contacts() && isFollowing() === undefined}>
<p>checking...</p>
</Match>
<Match when={contacts() && isFollowing() === true}>
<p style="color: green; font-weight: bold; font-size: 2rem;">
Congratulations! You are a follower of the fiatjaf
</p>
</Match>
<Match when={contacts() && isFollowing() === false}>
<p style="color: red; font-weight: bold; font-size: 2rem;">
Why don't you follow the fiatjaf? do you even like nostr?
</p>
</Match>
</Switch>
{/* ... */}
</div>
</>
);
}
13. Adding the follow button
Now that we have a way to check if the user is following fiatjaf, we should add a button to follow him. We can do this with Actions which are pre-built methods to modify nostr events for a user.
First we need to install the applesauce-actions and applesauce-factory package
pnpm install applesauce-actions applesauce-factory
Then create a src/actions.ts file with
import { EventFactory } from "applesauce-factory";
import { ActionHub } from "applesauce-actions";
import { eventStore } from "./stores";
import { accounts } from "./accounts";
// The event factory is used to build and modify nostr events
export const factory = new EventFactory({
// accounts.signer is a NIP-07 signer that signs with the currently active account
signer: accounts.signer,
});
// The action hub is used to run Actions against the event store
export const actions = new ActionHub(eventStore, factory);
Then create a toggleFollow method that will add or remove fiatjaf from the users contacts.
We are using the
execmethod to run the action, and theforEachmethod from RxJS allows us to await for all the events to be published
function App() {
// ...
const toggleFollow = async () => {
// send any created events to rxNostr and the event store
const publish = (event: NostrEvent) => {
eventStore.add(event);
rxNostr.send(event);
};
if (isFollowing()) {
await actions
.exec(UnfollowUser, "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d")
.forEach(publish);
} else {
await actions
.exec(
FollowUser,
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
"wss://pyramid.fiatjaf.com/"
)
.forEach(publish);
}
};
// ...
return (
<>
{/* ... */}
<div class="card">
{/* ... */}
{contacts() && <button onClick={toggleFollow}>{isFollowing() ? "Unfollow" : "Follow"}</button>}
</div>
</>
);
}
14. Adding outbox support
The app looks like it works now but if the user reloads the page they will still see an the old version of their contacts list. we need to make sure rxNostr is publishing the events to the users outbox relays.
To do this we can subscribe to the signed in users mailboxes using the query store in src/nostr.ts
import { MailboxesQuery } from "applesauce-core/queries";
import { accounts } from "./accounts";
import { of, switchMap } from "rxjs";
import { queryStore } from "./stores";
// ...
// subscribe to the active account, then subscribe to the users mailboxes and update rxNostr
accounts.active$
.pipe(switchMap((account) => (account ? queryStore.createQuery(MailboxesQuery, account.pubkey) : of(undefined))))
.subscribe((mailboxes) => {
if (mailboxes) rxNostr.setDefaultRelays(mailboxes.outboxes);
else rxNostr.setDefaultRelays([]);
});
And that’s it! we have a working nostr app that lets users follow and unfollow fiatjaf.