Dance Therapy Logs
Pin JSON and image records of dance therapy sessions on IPFS for secure patient histories.
Pinata IPFS Pin· storage
Section · Onchain
full primer →The primitive.
Every health documentation artefact is pinned to IPFS through Pinata; choreographers get a permanent CID plus a Solana transaction that timestamps it on Devnet.
Why this primitiveIPFS ensures confidential, permanent, and decentralized health session storage.
Kernel
User-generated artefacts are pinned to IPFS via a Pinata JWT and referenced by CID inside the Solana transaction memo.
Drives the UI as
Every submission surfaces its CID with a live `gateway.pinata.cloud/ipfs/<cid>` preview alongside the Solana Explorer link.
Required keys.
SOLANA_RPC_URL
Alchemy Solana Devnet RPC (https://solana-devnet.g.alchemy.com/v2/<KEY>). The public api.devnet.solana.com is rate-limited and CORS-blocked in the browser.
open ↗PHANTOM_PRIVATE_KEY
Base58 secret key exported from Phantom (Settings → Export Private Key). Used ONCE by scripts/deploy.ts to pay rent + fees for your bespoke program on Devnet. Fund it from https://faucet.solana.com. Never shipped to the browser.
open ↗Add these in your Lovable project under Settings → Secrets before pasting the prompt below.
Appendix · Mega-prompt
The build prompt.
budget · 1 message
Paste into a fresh Lovable project. Make sure all five secrets above are set first. read the build strategy →
Build "Dance Therapy Logs" in ONE Lovable message. Single-page demo on Solana Devnet. This prompt
is self-contained — do NOT rely on any external Solana skill or docs beyond what is inlined here.
CONCEPT
Pin JSON and image records of dance therapy sessions on IPFS for secure patient histories.
Discipline: Dance & Choreography (health documentation).
Onchain primitive: Pinata IPFS Pin. Why this primitive: IPFS ensures confidential, permanent, and decentralized health session storage.
5-CREDIT BUDGET (HARD LIMIT — one Lovable message, no follow-ups):
- ONE single-page app (index route only). No router, no Lovable Cloud, no database, no auth flows
beyond the Privy drop-in.
- ONE bespoke Solana program deployed to Devnet FROM THE SANDBOX during this build (see DEPLOY),
then called from the browser via Privy's embedded wallet. No Anchor framework — plain
`solana-program` crate keeps the .so under 100 KB and rent under 0.5 SOL.
- Privy is ALWAYS the browser auth + signing layer (Google login, embedded Solana wallet on Devnet).
- Pinata/IPFS only if the idea genuinely needs to store a file or JSON metadata.
- At most ONE `LOVABLE_API_KEY` call per user action, invoked from a server function (never the browser).
- Skip tests, skip CI, skip docs pages. Ship the demo, nothing else.
REQUIRED SECRETS (Lovable -> Project Settings -> Secrets — add BEFORE sending this prompt):
- LOVABLE_API_KEY Auto-provisioned by Lovable. One AI call per user action, max.
- PRIVY_APP_ID From https://dashboard.privy.io. In the SAME Privy app, whitelist BOTH
`https://*.lovable.app` AND your published `https://<slug>.lovable.app`
under Allowed Origins, or the signing iframe is killed by `frame-ancestors`
CSP and every tx fails with a blank "Something went wrong" modal.
Enable "Solana gas sponsorship" for cluster `solana:devnet` in the same
dashboard so users never see a fund-wallet prompt.
- PINATA_JWT Only required if the app pins media/metadata to IPFS.
Get one at https://app.pinata.cloud (JWT, not the legacy API key pair).
- SOLANA_RPC_URL Alchemy Solana Devnet RPC: https://solana-devnet.g.alchemy.com/v2/<KEY>.
The public https://api.devnet.solana.com is rate-limited and blocks
browser CORS on getLatestBlockhash — do NOT use it.
- PHANTOM_PRIVATE_KEY Base58 secret key exported from YOUR Phantom wallet (Settings -> Security
& Privacy -> Show Secret Recovery Phrase -> Export Private Key). Fund it
with ~2 Devnet SOL from https://faucet.solana.com BEFORE sending this
prompt. Lovable uses this key EXACTLY ONCE inside the sandbox during this
build to `solana program deploy` the bespoke program, then wipes it. The
browser bundle must never import it.
STACK
- React + Vite single page (the index route only).
- `@solana/web3.js` for tx building. `@privy-io/react-auth` v3+ for auth AND signing.
- Cluster: Devnet ONLY. Never mainnet. Use the Alchemy `SOLANA_RPC_URL` everywhere — in the
frontend Connection, in `PrivyProvider.solana.rpcs`, and in the sandbox `solana config`.
- Devnet gas is sponsored by Privy (dashboard setting). Never call `requestAirdrop`, never render
a "fund wallet" UI.
VITE + BUFFER POLYFILL (4 layers — omit any one and it works in dev but blanks the
published mobile build):
1. `bun add buffer vite-plugin-node-polyfills`.
2. `vite.config.ts`:
```ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { nodePolyfills } from "vite-plugin-node-polyfills";
export default defineConfig({
plugins: [react(), nodePolyfills({ globals: { Buffer: true, global: true }, protocolImports: true })],
optimizeDeps: { esbuildOptions: { define: { global: "globalThis" } } },
resolve: { alias: { "rpc-websockets/dist/lib/client": "rpc-websockets/dist/lib/client.browser.js" } },
});
```
DO NOT add a top-level `define: { global: "globalThis" }` in vite.config — it conflicts with the
plugin's Rollup inject and leaves Buffer undefined in dependency chunks. DO NOT put `import`
statements inside `build.rollupOptions.output.intro` — top-level import inside a chunk wrapper is
illegal and blanks the page.
3. `index.html` — inline BEFORE any bundle loads:
```html
<script type="module">
import { Buffer } from "https://esm.sh/buffer@6";
globalThis.Buffer = globalThis.Buffer || Buffer;
</script>
```
4. `src/polyfills.ts` (imported FIRST in `src/main.tsx`, before anything else):
```ts
import { Buffer } from "buffer";
(globalThis as any).Buffer = (globalThis as any).Buffer || Buffer;
(window as any).Buffer = (window as any).Buffer || Buffer;
```
Then in every file that touches Solana, add `import { Buffer } from "buffer";` at the top — the
plugin cannot inject into your own TypeScript source.
PRIVY (browser auth + Solana signing) — Privy v3, MUST include `solana.rpcs`:
Install: `bun add @privy-io/react-auth @solana/web3.js @solana/kit @solana-program/memo`
(`@solana-program/memo` is an implicit peer dep of `@privy-io/react-auth/solana`; missing it
crashes the Privy modal at runtime).
`src/components/PrivyRoot.tsx` (single-page app, no SSR concerns here, but keep imports scoped):
```tsx
import { PrivyProvider } from "@privy-io/react-auth";
import { toSolanaWalletConnectors } from "@privy-io/react-auth/solana";
import { createSolanaRpc, createSolanaRpcSubscriptions } from "@solana/kit";
const rpcUrl = import.meta.env.VITE_SOLANA_RPC_URL!; // set from SOLANA_RPC_URL secret
const wsUrl = rpcUrl.replace("https://", "wss://");
export function PrivyRoot({ children }: { children: React.ReactNode }) {
return (
<PrivyProvider
appId={import.meta.env.VITE_PRIVY_APP_ID!}
config={{
loginMethods: ["google", "email"],
embeddedWallets: { solana: { createOnLogin: "users-without-wallets" } },
externalWallets: { solana: { connectors: toSolanaWalletConnectors() } },
solanaClusters: [{ name: "devnet", rpcUrl }],
solana: {
rpcs: {
"solana:devnet": {
rpc: createSolanaRpc(rpcUrl),
rpcSubscriptions: createSolanaRpcSubscriptions(wsUrl),
},
},
},
}}
>{children}</PrivyProvider>
);
}
```
Without the `solana.rpcs` mapping, Privy throws "No RPC configuration found for chain solana:devnet"
the moment the user tries to sign.
Expose the secrets to the browser in `.env.local` (Lovable already mirrors non-VITE secrets, but
Vite only ships `VITE_*` to the client):
```
VITE_PRIVY_APP_ID=<same value as PRIVY_APP_ID secret>
VITE_SOLANA_RPC_URL=<same value as SOLANA_RPC_URL secret>
```
Sign + send with sponsorship:
```tsx
import { useSolanaWallets, useSignAndSendTransaction } from "@privy-io/react-auth/solana";
const { wallets } = useSolanaWallets();
const embedded = wallets.find(w => w.walletClientType === "privy");
const { signAndSendTransaction } = useSignAndSendTransaction();
const { signature } = await signAndSendTransaction(tx, {
address: embedded!.address, // pin to embedded wallet — else Privy routes to an external one
sponsor: true, // opts into the dashboard's gas sponsorship
});
```
Never call `connection.requestAirdrop` and never render a "fund your wallet" UI — sponsorship
covers everything on Devnet.
DEPLOY (Lovable runs this ONCE, inside the sandbox, during THIS build message —
it counts toward the 5-credit budget, so make it the first thing you do):
1. Create `solana/program/src/lib.rs` — a tiny native Rust program (no Anchor) with on-chain
security metadata so Explorer's Security tab shows contact + source URL even before OtterSec
verification lands:
```rust
use solana_program::{account_info::AccountInfo, entrypoint, entrypoint::ProgramResult,
msg, pubkey::Pubkey};
#[cfg(not(feature = "no-entrypoint"))]
use solana_security_txt::security_txt;
#[cfg(not(feature = "no-entrypoint"))]
security_txt! {
name: "<your-program-name>",
project_url: "https://<your-slug>.lovable.app",
contacts: "email:you@example.com",
policy: "https://github.com/<user>/<repo>/blob/main/SECURITY.md",
source_code: "https://github.com/<user>/<repo>",
preferred_languages: "en",
auditors: "None"
}
entrypoint!(process);
pub fn process(_pid: &Pubkey, _accs: &[AccountInfo], data: &[u8]) -> ProgramResult {
msg!("payload: {}", std::str::from_utf8(data).unwrap_or("<binary>"));
Ok(())
}
```
and `solana/program/Cargo.toml` with `[lib] crate-type = ["cdylib", "lib"]`,
`[features] no-entrypoint = []`, and dependencies `solana-program = "1.18"`,
`solana-security-txt = "1.1"`.
Also create `solana/program/rust-toolchain.toml` pinning the channel — this is REQUIRED for
`solana-verify --remote` to produce the same hash as on-chain:
```toml
[toolchain]
channel = "1.89.0"
```
2. In the sandbox shell (run in this exact order):
```bash
# install toolchains if missing
sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"
export PATH="$HOME/.local/share/solana/install/active_release/bin:$PATH"
bun add bs58
# materialise the deployer keypair from the Phantom secret
node -e '
const bs58 = require("bs58");
const fs = require("fs");
const secret = bs58.decode(process.env.PHANTOM_PRIVATE_KEY);
fs.writeFileSync("/tmp/deployer.json", JSON.stringify(Array.from(secret)));
'
solana config set --url "$SOLANA_RPC_URL" --keypair /tmp/deployer.json
solana balance # sanity check — must be >= 2 SOL
# build + deploy (commit Cargo.lock afterwards — verification needs it)
cargo build-sbf --manifest-path solana/program/Cargo.toml
PROGRAM_ID=$(solana program deploy \
solana/program/target/deploy/*.so --output json | jq -r .programId)
echo "export const PROGRAM_ID = \"$PROGRAM_ID\";" > src/lib/program-id.ts
# wipe the deployer key — the browser bundle must never see it
rm -f /tmp/deployer.json
```
3. Frontend imports `PROGRAM_ID` from `src/lib/program-id.ts`. The Program ID is the only Solana
program identifier that ever reaches the browser bundle.
VERIFY (reproducible build badge on Solana Explorer) — do NOT attempt submission
from the sandbox; instead ship the artifacts so the user can run it themselves once their repo is
public on GitHub:
1. Commit `solana/program/rust-toolchain.toml` AND `solana/program/Cargo.lock` — OtterSec's remote
builder needs both to reproduce the exact on-chain hash. A missing lockfile or unpinned toolchain
returns "hash mismatch" every time.
2. Create `SECURITY.md` at the repo root with a contact email and this "How to verify" section.
3. Create `solana/program/VERIFY.md` with the exact command, program ID substituted in:
```
solana-verify verify-from-repo --remote \
--url https://api.devnet.solana.com \
--program-id <PROGRAM_ID> \
--library-name <snake_case_crate_name> \
--mount-path solana/program \
--keypair ~/.config/solana/id.json \
https://github.com/<user>/<repo>
```
4. Add `src/components/verification-card.tsx` that polls `https://verify.osec.io/status/<PROGRAM_ID>`
every 30s and renders "Reproducible build ✓ verified" (emerald) when `is_verified` is true,
"Verification pending" (amber) otherwise. Render it on the same page as the program demo.
Hard requirements the user must know about (put them in `VERIFY.md`):
- Repo MUST be **public** on GitHub. OtterSec clones anonymously; private repos fail with a clone
error and Explorer will never show the green badge.
- The `~/.config/solana/id.json` on the user's machine MUST be the same upgrade authority that
deployed the program (i.e. the Phantom key they used as `PHANTOM_PRIVATE_KEY`).
- Verification is Docker-free thanks to `--remote` (OtterSec builds server-side). The user does
NOT need Docker installed.
CALL THE DEPLOYED PROGRAM (`src/lib/tx.ts`) — order of `keys` MUST match
your Rust `#[derive(Accounts)]` field-for-field (a swap surfaces as `AccountNotInitialized` or a bare
signature-verification error with no useful message):
```ts
import { Buffer } from "buffer";
import { Connection, PublicKey, Transaction, TransactionInstruction } from "@solana/web3.js";
import { PROGRAM_ID } from "./program-id";
export function connection() {
return new Connection(import.meta.env.VITE_SOLANA_RPC_URL!, "confirmed");
}
export async function buildTx(payer: PublicKey, payload: string) {
const ix = new TransactionInstruction({
keys: [{ pubkey: payer, isSigner: true, isWritable: true }], // order is load-bearing
programId: new PublicKey(PROGRAM_ID),
data: Buffer.from(payload, "utf8"),
});
const { blockhash } = await connection().getLatestBlockhash();
const tx = new Transaction({ recentBlockhash: blockhash, feePayer: payer }).add(ix);
return tx;
}
```
Do NOT install `@coral-xyz/anchor` — it drags in Node-only deps that fight Vite. Build instructions
by hand as above. Explorer link format: `https://explorer.solana.com/tx/<sig>?cluster=devnet`.
PINATA IPFS UPLOAD (`src/lib/pinata.ts`) — server function, never expose the JWT to the browser:
```ts
import { createServerFn } from "@tanstack/react-start";
export const pinToIpfs = createServerFn({ method: "POST" })
.inputValidator((d) => d as { name: string; contentBase64: string; mime: string })
.handler(async ({ data }) => {
const bytes = Uint8Array.from(atob(data.contentBase64), c => c.charCodeAt(0));
const fd = new FormData();
fd.append("file", new Blob([bytes], { type: data.mime }), data.name);
const res = await fetch("https://api.pinata.cloud/pinning/pinFileToIPFS", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.PINATA_JWT}` },
body: fd,
});
if (!res.ok) throw new Error(`pinata ${res.status}`);
const j = await res.json();
return { cid: j.IpfsHash as string };
});
```
Preview URL: `https://gateway.pinata.cloud/ipfs/<cid>`.
USER FLOW
1. Land on page -> "Continue with Google" (Privy) -> embedded Solana wallet auto-provisioned,
Devnet gas sponsored — no airdrop or funding UI.
2. On submit, upload the health documentation artefact via `pinToIpfs`, then call the bespoke program with the CID as payload via `useSignAndSendTransaction`. Render the CID, IPFS gateway preview, and Solana Explorer tx link.
3. Footer renders the credit line below.
COMMON GOTCHAS (each one has bitten prior participants — read before shipping):
- "Buffer is not defined" on published mobile only: you skipped one of the 4 polyfill layers above.
- Privy modal shows "Something went wrong" and console has a `frame-ancestors` CSP error:
add your `*.lovable.app` origin under Privy Dashboard -> Allowed Origins for the SAME app whose
ID is in `PRIVY_APP_ID`.
- Privy modal shows "No RPC configuration found for chain solana:devnet": you forgot the
`solana.rpcs` mapping in `PrivyProvider` config.
- `signAndSendTransaction` succeeds but user gets a wallet-connect prompt: pass
`{ address: embedded.address, sponsor: true }` — omitting `address` lets Privy route to an
external wallet.
- `429 Too Many Requests` / CORS errors from `api.devnet.solana.com`: switch `SOLANA_RPC_URL` to
Alchemy Devnet.
- `cargo build-sbf: command not found`: install the Anza CLI (see DEPLOY step 2).
- Program deploy fails with "insufficient funds for rent": the wallet backing
`PHANTOM_PRIVATE_KEY` isn't funded — hit https://faucet.solana.com and re-run just the deploy step.
- OtterSec verification returns "hash mismatch": `rust-toolchain.toml` or `Cargo.lock` isn't
committed, or the repo isn't public. Fix all three and re-run the `solana-verify` command.
CREDIT (must appear in UI footer AND, if you mint an NFT, inside the pinned metadata `description` field):
Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
Market sizing.
TAM
$5B
dance therapy market
SAM
$90M
therapy practice software
SOM
$9M
patient session archiving
Indicative figures for hackathon pitches — refine with your own research before raising.
Adjacent entries.
dance notation archiving
Choreo Sequence Vault
Securely store and share choreographic sequences as immutable IPFS manifests for global access.
dance inspiration boardsMovement Moodboard
Create and pin curated dance inspiration boards with images and JSON metadata on IPFS.
digital dance artDance NFT Gallery
Artists mint and pin digital dance art portfolios as NFTs stored forever on IPFS.
IP rights managementChoreographer’s Ledger
Record choreography permissions and usage rights as JSON manifests pinned on IPFS.