Sol.College

Full lesson

CypherMed

A decentralized protocol for patient-controlled medical records. Immutable audit trails on Solana, encrypted data off-chain. Giving patients true ownership, hospitals seamless compliance, and insurers fraud-proof verification.

What it does

From the project README:

CypherMed is a Solana-based medical records MVP focused on patient-controlled, auditable access.
Original
StaySafe020/CypherMed
SuperStack fork
Blockchains/CypherMed @ 7214758
Stars
3
Last push (original)
2026-10-15
Languages
TypeScript, Rust, CSS, HTML, JavaScript
Detected stack
@solana/kitAnchorNext.jsweb3.js
Categories
Other
Licence
Apache-2.0
Homepage
https://cyphermed.vercel.app

Architecture

75 source files. By extension: .ts ×28, .rs ×25, .tsx ×17, .js ×5.

Top-level directorySource files
programs25
app25
backend22
tests2
migrations1

On-chain programs: programs/cyphermed

Dapp: devnet deploy pending

⏳ Devnet deploy pending — goes live when the devnet wallet is topped up. This fork's own dapp is not deployed yet, so there is no program address or transaction to show. It is queued in dapp-queue.txt; once devnet SOL is available it is deployed, exercised with real devnet transactions, and this section switches to the live dapp at /dapps/cyphermed/. Until then, the lesson and step-by-step guide below are complete, and you can run it locally with the commands below.

Key code, explained

Excerpts chosen automatically where the code uses the most Solana concepts, pinned to the fork's commit.

StaySafe020/CypherMed · programs/cyphermed/src/instructions/consent_delegation.rs L121–144 @ 7214758 · Apache-2.0 · original
    emit!(DelegationRevokedEvent {        patient: patient.key(),        delegate: delegate_account.delegate,        revoked_by: ctx.accounts.authority.key(),        timestamp: clock.unix_timestamp,    });     Ok(())} #[derive(Accounts)]pub struct DelegateConsent<'info> {    #[account(        mut,        seeds = [b"patient", authority.key().as_ref()],        bump = patient.bump,        has_one = authority @ CypherMedError::Unauthorized    )]    pub patient: Account<'info, Patient>,     #[account(        init,        payer = authority,        space = ConsentDelegate::LEN,
  1. Line 121 emit!(DelegationRevokedEvent {: Emits an Anchor event that clients can parse from the logs.
  2. Line 125 timestamp: clock.unix_timestamp,: Reads on-chain time from the Clock sysvar.
  3. Line 135 seeds = [b"patient", authority.key().as_ref()],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  4. Line 136 bump = patient.bump,: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
  5. Line 137 has_one = authority @ CypherMedError::Unauthorized: Checks that a stored pubkey field matches the passed account.
  6. Line 142 init,: Creates and pays for a new account. Space and payer must be specified.
StaySafe020/CypherMed · programs/cyphermed/src/instructions/grant_access.rs L61–84 @ 7214758 · Apache-2.0 · original
     // Update patient access grant count    patient.access_grant_count = patient.access_grant_count.checked_add(1).unwrap();    patient.updated_at = clock.unix_timestamp;     msg!(        "Access granted to provider: {} by patient: {} with role: {:?}",        ctx.accounts.provider.key(),        patient.key(),        role    );     Ok(())} #[derive(Accounts)]pub struct GrantAccess<'info> {    #[account(        mut,        seeds = [b"patient", authority.key().as_ref()],        bump = patient.bump,        has_one = authority @ CypherMedError::Unauthorized    )]    pub patient: Account<'info, Patient>,
  1. Line 63 patient.access_grant_count = patient.access_grant_count.checked_add(1).unwrap();: Uses checked arithmetic so overflow fails instead of wrapping.
  2. Line 64 patient.updated_at = clock.unix_timestamp;: Reads on-chain time from the Clock sysvar.
  3. Line 66 msg!(: Logs to the program log, which is visible in explorers and the test output.
  4. Line 80 seeds = [b"patient", authority.key().as_ref()],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  5. Line 81 bump = patient.bump,: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
  6. Line 82 has_one = authority @ CypherMedError::Unauthorized: Checks that a stored pubkey field matches the passed account.
StaySafe020/CypherMed · programs/cyphermed/src/instructions/patient_management.rs L61–84 @ 7214758 · Apache-2.0 · original
     emit!(PatientReactivatedEvent {        patient: patient.key(),        authority: ctx.accounts.authority.key(),        timestamp: clock.unix_timestamp,    });     Ok(())} #[derive(Accounts)]pub struct UpdatePatient<'info> {    #[account(        mut,        seeds = [b"patient", authority.key().as_ref()],        bump = patient.bump,        has_one = authority @ CypherMedError::Unauthorized    )]    pub patient: Account<'info, Patient>,     pub authority: Signer<'info>,} #[derive(Accounts)]
  1. Line 62 emit!(PatientReactivatedEvent {: Emits an Anchor event that clients can parse from the logs.
  2. Line 65 timestamp: clock.unix_timestamp,: Reads on-chain time from the Clock sysvar.
  3. Line 75 seeds = [b"patient", authority.key().as_ref()],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  4. Line 76 bump = patient.bump,: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
  5. Line 77 has_one = authority @ CypherMedError::Unauthorized: Checks that a stored pubkey field matches the passed account.
  6. Line 81 pub authority: Signer<'info>,: Requires this account to have signed the transaction.
StaySafe020/CypherMed · programs/cyphermed/src/instructions/approve_deny_request.rs L73–96 @ 7214758 · Apache-2.0 · original
    access_grant.can_view = can_view;    access_grant.reason = access_request.reason.clone();    access_grant.revoked_by = None;    access_grant.revoked_at = None;    access_grant.bump = ctx.bumps.access_grant;     // Update patient stats    patient.access_grant_count = patient.access_grant_count.checked_add(1).unwrap();    patient.updated_at = clock.unix_timestamp;     msg!(        "Access request approved: {} granted access to patient {}",        access_request.requester,        patient.key()    );     emit!(AccessRequestApprovedEvent {        request: access_request.key(),        patient: patient.key(),        provider: access_request.requester,        access_grant: access_grant.key(),    });     Ok(())
  1. Line 77 access_grant.bump = ctx.bumps.access_grant;: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
  2. Line 80 patient.access_grant_count = patient.access_grant_count.checked_add(1).unwrap();: Uses checked arithmetic so overflow fails instead of wrapping.
  3. Line 81 patient.updated_at = clock.unix_timestamp;: Reads on-chain time from the Clock sysvar.
  4. Line 83 msg!(: Logs to the program log, which is visible in explorers and the test output.
  5. Line 89 emit!(AccessRequestApprovedEvent {: Emits an Anchor event that clients can parse from the logs.

How to run, build and test

These commands come from the repo's own manifests (package.json scripts, Anchor.toml, Cargo, go.mod, Makefile). Check the README for prerequisites and environment variables.

DirectoryCommandRuns
.npm run lintprettier "app/*.js" "app/src/**/*.{js,ts,tsx}" "backend/**/*.{js,ts,tsx}" "migrations/**/*.{js,ts,tsx}" "tests/**/*.{js,
backendnpm run devts-node-dev --respawn --transpile-only src/index.ts
backendnpm run startnode dist/index.js
backendnpm run buildtsc
backendnpm run testnode --require ts-node/register/transpile-only --test test/auth.test.ts
appnpm run devnext dev -p 3001
appnpm run startnext start
appnpm run buildnext build
appnpm run lintnext lint
.anchor buildcompile programs and generate IDLs
.anchor testlocal validator + test suite
.anchor run testyarn run ts-mocha -p ./tsconfig.json -t 1000000 \
.cargo buildRust workspace build
.cargo testRust tests

What you could build from it

Used in lessons