Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
radicle: Refactor COB Storage Access
Lorenz Leutgeb committed 29 days ago
commit f223afd9d7eb4d1301c7f6fe98bc06418c35c3f5
parent 10a8295
48 files changed +1241 -1290
modified crates/radicle-cli/src/commands/cob.rs
@@ -11,6 +11,8 @@ use nonempty::NonEmpty;

use radicle::cob;
use radicle::cob::store::CobAction;
+
use radicle::cob::store::access::ReadOnly;
+
use radicle::cob::store::access::WriteAs;
use radicle::cob::stream::CobStream as _;
use radicle::git;
use radicle::prelude::*;
@@ -48,21 +50,22 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
            type_name,
            operation,
        }) => {
-
            let signer = &profile.signer()?;
+
            let signer = profile.signer()?;
+
            let access = WriteAs::new(&signer);
            let repo = storage.repository_mut(repo)?;
            let embeds = embeds(&repo, operation.embed_files, operation.embed_hashes)?;

            let oid = match type_name {
                Patch => {
-
                    let store: Store<cob::patch::Patch, _> = Store::open(&repo)?;
+
                    let mut store: Store<cob::patch::Patch, _, _> = Store::open(&repo, access)?;
                    let actions = read_jsonl_actions(&operation.actions)?;
-
                    let (oid, _) = store.create(&operation.message, actions, embeds, signer)?;
+
                    let (oid, _) = store.create(&operation.message, actions, embeds)?;
                    oid
                }
                Issue => {
-
                    let store: Store<cob::issue::Issue, _> = Store::open(&repo)?;
+
                    let mut store: Store<cob::issue::Issue, _, _> = Store::open(&repo, access)?;
                    let actions = read_jsonl_actions(&operation.actions)?;
-
                    let (oid, _) = store.create(&operation.message, actions, embeds, signer)?;
+
                    let (oid, _) = store.create(&operation.message, actions, embeds)?;
                    oid
                }
                Identity => anyhow::bail!(
@@ -70,10 +73,10 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
                    &type_name
                ),
                Other(type_name) => {
-
                    let store: Store<cob::external::External, _> =
-
                        Store::open_for(&type_name, &repo)?;
+
                    let mut store: Store<cob::external::External, _, _> =
+
                        Store::open_for(&type_name, &repo, access)?;
                    let actions = read_jsonl_actions(&operation.actions)?;
-
                    let (oid, _) = store.create(&operation.message, actions, embeds, signer)?;
+
                    let (oid, _) = store.create(&operation.message, actions, embeds)?;
                    oid
                }
            };
@@ -178,9 +181,9 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
                Patch => {
                    let actions: Vec<cob::patch::Action> =
                        read_jsonl_actions(&operation.actions)?.into();
-
                    let mut patches = profile.patches_mut(&repo)?;
+
                    let mut patches = crate::terminal::cob::patches_mut(&profile, &repo, signer)?;
                    let mut patch = patches.get_mut(&oid)?;
-
                    patch.transaction(&operation.message, &*profile.signer()?, |tx| {
+
                    patch.transaction(&operation.message, |tx| {
                        tx.extend(actions)?;
                        tx.embed(embeds)?;
                        Ok(())
@@ -189,9 +192,9 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
                Issue => {
                    let actions: Vec<cob::issue::Action> =
                        read_jsonl_actions(&operation.actions)?.into();
-
                    let mut issues = profile.issues_mut(&repo)?;
+
                    let mut issues = crate::terminal::cob::issues_mut(&profile, &repo, signer)?;
                    let mut issue = issues.get_mut(&oid)?;
-
                    issue.transaction(&operation.message, &*profile.signer()?, |tx| {
+
                    issue.transaction(&operation.message, |tx| {
                        tx.extend(actions)?;
                        tx.embed(embeds)?;
                        Ok(())
@@ -204,9 +207,10 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
                Other(type_name) => {
                    use cob::external::{Action, External};
                    let actions: Vec<Action> = read_jsonl_actions(&operation.actions)?.into();
-
                    let mut store: Store<External, _> = Store::open_for(&type_name, &repo)?;
+
                    let mut store: Store<External, _, _> =
+
                        Store::open_for(&type_name, &repo, WriteAs::new(signer))?;
                    let tx = cob::store::Transaction::new(type_name.clone(), actions, embeds);
-
                    let (_, oid) = tx.commit(&operation.message, oid, &mut store, signer)?;
+
                    let (_, oid) = tx.commit(&operation.message, oid, &mut store)?;
                    oid
                }
            };
@@ -244,6 +248,7 @@ fn show(
        }
        FilteredTypeName::Issue => {
            use radicle::issue::cache::Issues as _;
+

            let issues = term::cob::issues(profile, repo)?;
            for oid in oids {
                let oid = &oid.resolve(&repo.backend)?;
@@ -259,6 +264,7 @@ fn show(
        }
        FilteredTypeName::Patch => {
            use radicle::patch::cache::Patches as _;
+

            let patches = term::cob::patches(profile, repo)?;
            for oid in oids {
                let oid = &oid.resolve(&repo.backend)?;
@@ -273,8 +279,9 @@ fn show(
            }
        }
        FilteredTypeName::Other(type_name) => {
-
            let store =
-
                cob::store::Store::<cob::external::External, _>::open_for(&type_name, repo)?;
+
            let store = cob::store::Store::<cob::external::External, _, _>::open_for(
+
                &type_name, repo, ReadOnly,
+
            )?;
            for oid in oids {
                let oid = &oid.resolve(&repo.backend)?;
                let cob = store
modified crates/radicle-cli/src/commands/id.rs
@@ -9,7 +9,6 @@ use radicle::cob::identity::{self, IdentityMut, Revision, RevisionId};
use radicle::identity::doc::update;
use radicle::identity::{Doc, Identity, RawDoc, doc};
use radicle::node::NodeId;
-
use radicle::node::device::Device;
use radicle::storage::{ReadStorage as _, WriteRepository};
use radicle::{Profile, cob, crypto};
use radicle_surf::diff::Diff;
@@ -37,7 +36,9 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
    let repo = storage
        .repository(rid)
        .context(anyhow!("repository `{rid}` not found in local storage"))?;
-
    let mut identity = Identity::load_mut(&repo)?;
+

+
    let device = profile.signer()?;
+
    let mut identity = Identity::load_mut(&repo, &device)?;
    let current = identity.current().clone();

    let interactive = args.interactive();
@@ -47,14 +48,13 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
        Command::Accept { revision } => {
            let revision = get(revision, &identity, &repo)?.clone();
            let id = revision.id;
-
            let signer = term::signer(&profile)?;

            if !revision.is_active() {
                anyhow::bail!("cannot vote on revision that is {}", revision.state);
            }

            if interactive.confirm(format!("Accept revision {}?", term::format::tertiary(id))) {
-
                identity.accept(&revision.id, &signer)?;
+
                identity.accept(&revision.id)?;

                if let Some(revision) = identity.revision(&id) {
                    // Update the canonical head to point to the latest accepted revision.
@@ -72,7 +72,6 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
        }
        Command::Reject { revision } => {
            let revision = get(revision, &identity, &repo)?.clone();
-
            let signer = term::signer(&profile)?;

            if !revision.is_active() {
                anyhow::bail!("cannot vote on revision that is {}", revision.state);
@@ -82,7 +81,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
                "Reject revision {}?",
                term::format::tertiary(revision.id)
            )) {
-
                identity.reject(revision.id, &signer)?;
+
                identity.reject(revision.id)?;

                if !args.quiet {
                    term::success!("Revision {} rejected", revision.id);
@@ -96,7 +95,6 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
            description,
        } => {
            let revision = get(revision, &identity, &repo)?.clone();
-
            let signer = term::signer(&profile)?;

            if !revision.is_active() {
                anyhow::bail!("revision can no longer be edited");
@@ -104,7 +102,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
            let Some((title, description)) = edit_title_description(title, description)? else {
                anyhow::bail!("revision title or description missing");
            };
-
            identity.edit(revision.id, title, description, &signer)?;
+
            identity.edit(revision.id, title, description)?;

            if !args.quiet {
                term::success!("Revision {} edited", revision.id);
@@ -193,15 +191,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
                }
                return Ok(());
            }
-
            let signer = term::signer(&profile)?;
-
            let revision = update(
-
                title,
-
                description,
-
                proposal,
-
                &mut identity,
-
                &signer,
-
                &profile,
-
            )?;
+
            let revision = update(title, description, proposal, &mut identity, &profile)?;

            if revision.is_accepted() && revision.parent == Some(current.id) {
                // Update the canonical head to point to the latest accepted revision.
@@ -253,7 +243,6 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
        }
        Command::Redact { revision } => {
            let revision = get(revision, &identity, &repo)?.clone();
-
            let signer = term::signer(&profile)?;

            if revision.is_accepted() {
                anyhow::bail!("cannot redact accepted revision");
@@ -262,7 +251,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
                "Redact revision {}?",
                term::format::tertiary(revision.id)
            )) {
-
                identity.redact(revision.id, &signer)?;
+
                identity.redact(revision.id)?;

                if !args.quiet {
                    term::success!("Revision {} redacted", revision.id);
@@ -422,21 +411,23 @@ and description.
    Ok(result)
}

-
fn update<R, G>(
+
fn update<Repo, Signer>(
    title: Option<Title>,
    description: Option<String>,
    doc: Doc,
-
    current: &mut IdentityMut<R>,
-
    signer: &Device<G>,
+
    current: &mut IdentityMut<Repo, Signer>,
    profile: &Profile,
) -> anyhow::Result<Revision>
where
-
    R: WriteRepository + cob::Store<Namespace = NodeId>,
-
    G: crypto::signature::Signer<crypto::Signature>,
+
    Repo: WriteRepository + cob::Store<Namespace = NodeId>,
+
    Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
    Signer: crypto::signature::Signer<crypto::Signature>,
+
    Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
+
    Signer: crypto::signature::Verifier<crypto::Signature>,
{
    if let Some((title, description)) = edit_title_description(title, description)? {
        let id = current
-
            .update(title, description, &doc, signer)
+
            .update(title, description, &doc)
            .map_err(|e| on_identity_err(e, profile))?;
        let revision = current
            .revision(&id)
modified crates/radicle-cli/src/commands/issue.rs
@@ -6,13 +6,13 @@ use anyhow::Context as _;

use radicle::cob::common::Label;
use radicle::cob::issue::{CloseReason, State};
+
use radicle::cob::store::access::WriteAs;
use radicle::cob::{Title, issue};

use radicle::Profile;
use radicle::crypto;
use radicle::issue::cache::Issues as _;
use radicle::node::NodeId;
-
use radicle::node::device::Device;
use radicle::prelude::Did;
use radicle::profile;
use radicle::storage;
@@ -48,7 +48,8 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
        .unwrap_or_else(|| Command::List(args.empty.into()));

    let announce = !args.no_announce && command.should_announce_for();
-
    let mut issues = term::cob::issues_mut(&profile, &repo)?;
+
    let signer = profile.signer()?;
+
    let mut issues = term::cob::issues_mut(&profile, &repo, &signer)?;

    match command {
        Command::Edit {
@@ -56,8 +57,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
            title,
            description,
        } => {
-
            let signer = term::signer(&profile)?;
-
            let issue = edit(&mut issues, &repo, id, title, description, &signer)?;
+
            let issue = edit(&mut issues, &repo, id, title, description)?;
            if !args.quiet {
                term::issue::show(&issue, issue.id(), Format::Header, args.verbose, &profile)?;
            }
@@ -68,7 +68,6 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
            labels,
            assignees,
        } => {
-
            let signer = term::signer(&profile)?;
            open(
                title,
                description,
@@ -77,7 +76,6 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
                args.verbose,
                args.quiet,
                &mut issues,
-
                &signer,
                &profile,
            )?;
        }
@@ -131,10 +129,9 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
        Command::State { id, target_state } => {
            let to: StateArg = target_state.into();
            let id = id.resolve(&repo.backend)?;
-
            let signer = term::signer(&profile)?;
            let mut issue = issues.get_mut(&id)?;
            let state = to.into();
-
            issue.lifecycle(state, &signer)?;
+
            issue.lifecycle(state)?;

            if !args.quiet {
                let success =
@@ -155,7 +152,6 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
        } => {
            let id = id.resolve(&repo.backend)?;
            if let Ok(mut issue) = issues.get_mut(&id) {
-
                let signer = term::signer(&profile)?;
                let comment_id = match comment_id {
                    Some(cid) => cid.resolve(&repo.backend)?,
                    None => *term::io::comment_select(&issue).map(|(cid, _)| cid)?,
@@ -164,11 +160,10 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
                    Some(reaction) => reaction,
                    None => term::io::reaction_select()?,
                };
-
                issue.react(comment_id, reaction, true, &signer)?;
+
                issue.react(comment_id, reaction, true)?;
            }
        }
        Command::Assign { id, add, delete } => {
-
            let signer = term::signer(&profile)?;
            let id = id.resolve(&repo.backend)?;
            let Ok(mut issue) = issues.get_mut(&id) else {
                anyhow::bail!("Issue `{id}` not found");
@@ -179,7 +174,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
                .chain(add.iter())
                .cloned()
                .collect::<Vec<_>>();
-
            issue.assign(assignees, &signer)?;
+
            issue.assign(assignees)?;
        }
        Command::Label { id, add, delete } => {
            let id = id.resolve(&repo.backend)?;
@@ -192,8 +187,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
                .chain(add.iter())
                .cloned()
                .collect::<Vec<_>>();
-
            let signer = term::signer(&profile)?;
-
            issue.label(labels, &signer)?;
+
            issue.label(labels)?;
        }
        Command::List(list_args) => {
            list(
@@ -206,8 +200,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
        }
        Command::Delete { id } => {
            let id = id.resolve(&repo.backend)?;
-
            let signer = term::signer(&profile)?;
-
            issues.remove(&id, &signer)?;
+
            issues.remove(&id)?;
        }
        Command::Cache { id, storage } => {
            let mode = if storage {
@@ -365,20 +358,22 @@ fn mk_issue_row(
    ]
}

-
fn open<R, G>(
+
fn open<Repo, Signer>(
    title: Option<Title>,
    description: Option<String>,
    labels: Vec<Label>,
    assignees: Vec<Did>,
    verbose: bool,
    quiet: bool,
-
    cache: &mut issue::Cache<issue::Issues<'_, R>, cob::cache::StoreWriter>,
-
    signer: &Device<G>,
+
    cache: &mut issue::Cache<'_, Repo, WriteAs<'_, Signer>, cob::cache::StoreWriter>,
    profile: &Profile,
) -> anyhow::Result<()>
where
-
    R: WriteRepository + cob::Store<Namespace = NodeId>,
-
    G: crypto::signature::Signer<crypto::Signature>,
+
    Repo: WriteRepository + cob::Store<Namespace = NodeId>,
+
    Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
    Signer: crypto::signature::Signer<crypto::Signature>,
+
    Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
+
    Signer: crypto::signature::Verifier<crypto::Signature>,
{
    let (title, description) = if let (Some(t), Some(d)) = (title.as_ref(), description.as_ref()) {
        (t.to_owned(), d.to_owned())
@@ -393,7 +388,6 @@ where
        labels.as_slice(),
        assignees.as_slice(),
        [],
-
        signer,
    )?;

    if !quiet {
@@ -402,17 +396,19 @@ where
    Ok(())
}

-
fn edit<'a, 'g, R, G>(
-
    issues: &'g mut issue::Cache<issue::Issues<'a, R>, cob::cache::StoreWriter>,
+
fn edit<'a, 'b, 'g, Repo, Signer>(
+
    issues: &'g mut issue::Cache<'a, Repo, WriteAs<'b, Signer>, cob::cache::StoreWriter>,
    repo: &storage::git::Repository,
    id: Rev,
    title: Option<Title>,
    description: Option<String>,
-
    signer: &Device<G>,
-
) -> anyhow::Result<issue::IssueMut<'a, 'g, R, cob::cache::StoreWriter>>
+
) -> anyhow::Result<issue::IssueMut<'a, 'b, 'g, Repo, Signer, cob::cache::StoreWriter>>
where
-
    R: WriteRepository + cob::Store<Namespace = NodeId>,
-
    G: crypto::signature::Signer<crypto::Signature>,
+
    Repo: WriteRepository + cob::Store<Namespace = NodeId>,
+
    Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
    Signer: crypto::signature::Signer<crypto::Signature>,
+
    Signer: radicle::crypto::signature::Signer<radicle::crypto::ssh::ExtendedSignature>,
+
    Signer: crypto::signature::Verifier<crypto::Signature>,
{
    let id = id.resolve(&repo.backend)?;
    let mut issue = issues.get_mut(&id)?;
@@ -421,7 +417,7 @@ where

    if title.is_some() || description.is_some() {
        // Editing by command line arguments.
-
        issue.transaction("Edit", signer, |tx| {
+
        issue.transaction("Edit", |tx| {
            if let Some(t) = title {
                tx.edit(t)?;
            }
@@ -442,7 +438,7 @@ where
        return Ok(issue);
    };

-
    issue.transaction("Edit", signer, |tx| {
+
    issue.transaction("Edit", |tx| {
        tx.edit(title)?;
        tx.edit_comment(comment_id, description, vec![])?;

modified crates/radicle-cli/src/commands/issue/cache.rs
@@ -1,6 +1,7 @@
use std::ops::ControlFlow;

use radicle::Profile;
+
use radicle::cob::store::access::ReadOnly;
use radicle::issue::IssueId;
use radicle::storage::ReadStorage as _;
use radicle::storage::git::Repository;
@@ -37,7 +38,18 @@ pub fn run(mode: CacheMode, profile: &Profile) -> anyhow::Result<()> {
}

fn cache(id: Option<IssueId>, repository: &Repository, profile: &Profile) -> anyhow::Result<()> {
-
    let mut issues = term::cob::issues_mut(profile, repository)?;
+
    let mut issues = {
+
        // NOTE: Since we require a cache that is writeable, on top of a store that
+
        // is read-only, we can neither use [`term::cob::issues_mut`] nor [`term::cob::issues`]
+
        // since these convenience functions pair a writeable cache with a writeable
+
        // store, and respectively a read-only cache with a read-only store.
+

+
        let db = profile.cobs_db_mut()?;
+
        db.check_version()?;
+
        let store = radicle::cob::issue::Issues::open(repository, ReadOnly)?;
+

+
        radicle::cob::issue::Cache::open(store, db)
+
    };

    match id {
        Some(id) => {
modified crates/radicle-cli/src/commands/issue/comment.rs
@@ -1,4 +1,5 @@
use radicle::Profile;
+
use radicle::cob::store::access::WriteAs;
use radicle::cob::thread;
use radicle::storage::WriteRepository;
use radicle::{cob, git, issue, storage};
@@ -8,27 +9,35 @@ use crate::terminal as term;
use crate::terminal::Element as _;
use crate::terminal::patch::Message;

-
pub(super) fn comment(
+
pub(super) fn comment<Signer>(
    profile: &Profile,
    repo: &storage::git::Repository,
    issues: &mut issue::Cache<
-
        issue::Issues<'_, storage::git::Repository>,
+
        '_,
+
        storage::git::Repository,
+
        WriteAs<'_, Signer>,
        cob::cache::Store<cob::cache::Write>,
    >,
    id: Rev,
    message: Message,
    reply_to: Option<Rev>,
    quiet: bool,
-
) -> Result<(), anyhow::Error> {
+
) -> Result<(), anyhow::Error>
+
where
+
    Signer: radicle::crypto::signature::Keypair<VerifyingKey = radicle::crypto::PublicKey>,
+
    Signer: radicle::crypto::signature::Signer<radicle::crypto::Signature>,
+
    Signer: radicle::crypto::signature::Signer<radicle::crypto::ssh::ExtendedSignature>,
+
    Signer: radicle::crypto::signature::Verifier<radicle::crypto::Signature>,
+
{
    let reply_to = reply_to
        .map(|rev| rev.resolve::<git::Oid>(repo.raw()))
        .transpose()?;
-
    let signer = term::signer(profile)?;
    let issue_id = id.resolve::<cob::ObjectId>(&repo.backend)?;
    let mut issue = issues.get_mut(&issue_id)?;
    let (root_comment_id, _) = issue.root();
    let body = prompt_comment(message, issue.thread(), reply_to, None)?;
-
    let comment_id = issue.comment(body, reply_to.unwrap_or(*root_comment_id), vec![], &signer)?;
+

+
    let comment_id = issue.comment(body, reply_to.unwrap_or(*root_comment_id), vec![])?;
    if quiet {
        term::print(comment_id);
    } else {
@@ -38,19 +47,26 @@ pub(super) fn comment(
    Ok(())
}

-
pub(super) fn edit(
+
pub(super) fn edit<Signer>(
    profile: &Profile,
    repo: &storage::git::Repository,
    issues: &mut issue::Cache<
-
        issue::Issues<'_, storage::git::Repository>,
+
        '_,
+
        storage::git::Repository,
+
        WriteAs<'_, Signer>,
        cob::cache::Store<cob::cache::Write>,
    >,
    id: Rev,
    message: Message,
    comment_id: Rev,
    quiet: bool,
-
) -> Result<(), anyhow::Error> {
-
    let signer = term::signer(profile)?;
+
) -> Result<(), anyhow::Error>
+
where
+
    Signer: radicle::crypto::signature::Keypair<VerifyingKey = radicle::crypto::PublicKey>,
+
    Signer: radicle::crypto::signature::Signer<radicle::crypto::Signature>,
+
    Signer: radicle::crypto::signature::Signer<radicle::crypto::ssh::ExtendedSignature>,
+
    Signer: radicle::crypto::signature::Verifier<radicle::crypto::Signature>,
+
{
    let issue_id = id.resolve::<cob::ObjectId>(&repo.backend)?;
    let comment_id = comment_id.resolve(&repo.backend)?;
    let mut issue = issues.get_mut(&issue_id)?;
@@ -64,7 +80,7 @@ pub(super) fn edit(
        comment.reply_to(),
        Some(comment.body()),
    )?;
-
    issue.edit_comment(comment_id, body, vec![], &signer)?;
+
    issue.edit_comment(comment_id, body, vec![])?;
    if quiet {
        term::print(comment_id);
    } else {
modified crates/radicle-cli/src/commands/patch/archive.rs
@@ -10,15 +10,15 @@ pub fn run(
    repository: &Repository,
) -> anyhow::Result<()> {
    let signer = term::signer(profile)?;
-
    let mut patches = term::cob::patches_mut(profile, repository)?;
+
    let mut patches = term::cob::patches_mut(profile, repository, &signer)?;
    let Ok(mut patch) = patches.get_mut(patch_id) else {
        anyhow::bail!("Patch `{patch_id}` not found");
    };

    if undo {
-
        patch.unarchive(&signer)?;
+
        patch.unarchive()?;
    } else {
-
        patch.archive(&signer)?;
+
        patch.archive()?;
    }

    Ok(())
modified crates/radicle-cli/src/commands/patch/assign.rs
@@ -15,7 +15,7 @@ pub fn run(
    repository: &Repository,
) -> anyhow::Result<()> {
    let signer = term::signer(profile)?;
-
    let mut patches = term::cob::patches_mut(profile, repository)?;
+
    let mut patches = term::cob::patches_mut(profile, repository, &signer)?;
    let Ok(mut patch) = patches.get_mut(patch_id) else {
        anyhow::bail!("Patch `{patch_id}` not found");
    };
@@ -24,6 +24,6 @@ pub fn run(
        .filter(|did| !delete.contains(did))
        .chain(add)
        .collect::<BTreeSet<_>>();
-
    patch.assign(assignees, &signer)?;
+
    patch.assign(assignees)?;
    Ok(())
}
modified crates/radicle-cli/src/commands/patch/cache.rs
@@ -1,6 +1,7 @@
use std::ops::ControlFlow;

use radicle::Profile;
+
use radicle::cob::store::access::ReadOnly;
use radicle::patch::PatchId;
use radicle::storage::ReadStorage as _;
use radicle::storage::git::Repository;
@@ -37,7 +38,18 @@ pub fn run(mode: CacheMode, profile: &Profile) -> anyhow::Result<()> {
}

fn cache(id: Option<PatchId>, repository: &Repository, profile: &Profile) -> anyhow::Result<()> {
-
    let mut patches = term::cob::patches_mut(profile, repository)?;
+
    let mut patches = {
+
        // NOTE: Since we require a cache that is writeable, on top of a store that
+
        // is read-only, we can neither use [`term::cob::patches_mut`] nor [`term::cob::patches`]
+
        // since these convenience functions pair a writeable cache with a writeable
+
        // store, and respectively a read-only cache with a read-only store.
+

+
        let db = profile.cobs_db_mut()?;
+
        db.check_version()?;
+
        let store = radicle::cob::patch::Patches::open(repository, ReadOnly)?;
+

+
        radicle::cob::patch::Cache::open(store, db)
+
    };

    match id {
        Some(id) => {
modified crates/radicle-cli/src/commands/patch/comment.rs
@@ -24,7 +24,7 @@ pub fn run(
    profile: &Profile,
) -> anyhow::Result<()> {
    let signer = term::signer(profile)?;
-
    let mut patches = term::cob::patches_mut(profile, repo)?;
+
    let mut patches = term::cob::patches_mut(profile, repo, &signer)?;

    let revision_id = revision_id.resolve::<cob::EntryId>(&repo.backend)?;
    let ByRevision {
@@ -37,7 +37,7 @@ pub fn run(
        .ok_or_else(|| anyhow!("Patch revision `{revision_id}` not found"))?;
    let mut patch = patch::PatchMut::new(patch_id, patch, &mut patches);
    let (body, reply_to) = prompt(message, reply_to, &revision, repo)?;
-
    let comment_id = patch.comment(revision_id, body, reply_to, None, vec![], &signer)?;
+
    let comment_id = patch.comment(revision_id, body, reply_to, None, vec![])?;
    let comment = patch
        .revision(&revision_id)
        .ok_or(anyhow!("error retrieving revision `{revision_id}`"))?
modified crates/radicle-cli/src/commands/patch/comment/edit.rs
@@ -21,7 +21,7 @@ pub fn run(
    profile: &Profile,
) -> anyhow::Result<()> {
    let signer = term::signer(profile)?;
-
    let mut patches = profile.patches_mut(repo)?;
+
    let mut patches = term::cob::patches_mut(profile, repo, &signer)?;
    let revision_id = revision_id.resolve::<cob::EntryId>(&repo.backend)?;
    let ByRevision {
        id: patch_id,
@@ -33,7 +33,7 @@ pub fn run(
        .ok_or_else(|| anyhow!("Patch revision `{revision_id}` not found"))?;
    let (body, _) = super::prompt(message, None, &revision, repo)?;
    let mut patch = patch::PatchMut::new(patch_id, patch, &mut patches);
-
    patch.comment_edit(revision_id, comment_id, body, vec![], &signer)?;
+
    patch.comment_edit(revision_id, comment_id, body, vec![])?;

    if !quiet {
        let comment = patch
modified crates/radicle-cli/src/commands/patch/comment/react.rs
@@ -21,7 +21,7 @@ pub fn run(
    profile: &Profile,
) -> anyhow::Result<()> {
    let signer = term::signer(profile)?;
-
    let mut patches = profile.patches_mut(repo)?;
+
    let mut patches = term::cob::patches_mut(profile, repo, &signer)?;
    let revision_id = revision_id.resolve::<cob::EntryId>(&repo.backend)?;
    let ByRevision {
        id: patch_id,
@@ -32,7 +32,7 @@ pub fn run(
        .find_by_revision(&patch::RevisionId::from(revision_id))?
        .ok_or_else(|| anyhow!("Patch revision `{revision_id}` not found"))?;
    let mut patch = patch::PatchMut::new(patch_id, patch, &mut patches);
-
    patch.comment_react(revision_id, comment, reaction, active, &signer)?;
+
    patch.comment_react(revision_id, comment, reaction, active)?;

    Ok(())
}
modified crates/radicle-cli/src/commands/patch/comment/redact.rs
@@ -18,7 +18,7 @@ pub fn run(
    profile: &Profile,
) -> anyhow::Result<()> {
    let signer = term::signer(profile)?;
-
    let mut patches = profile.patches_mut(repo)?;
+
    let mut patches = term::cob::patches_mut(profile, repo, &signer)?;
    let revision_id = revision_id.resolve::<cob::EntryId>(&repo.backend)?;
    let ByRevision {
        id: patch_id,
@@ -29,7 +29,7 @@ pub fn run(
        .find_by_revision(&patch::RevisionId::from(revision_id))?
        .ok_or_else(|| anyhow!("Patch revision `{revision_id}` not found"))?;
    let mut patch = patch::PatchMut::new(patch_id, patch, &mut patches);
-
    patch.comment_redact(revision_id, comment, &signer)?;
+
    patch.comment_redact(revision_id, comment)?;
    term::success!("Redacted comment {comment}");

    Ok(())
modified crates/radicle-cli/src/commands/patch/delete.rs
@@ -4,9 +4,9 @@ use radicle::storage::git::Repository;
use super::*;

pub fn run(patch_id: &PatchId, profile: &Profile, repository: &Repository) -> anyhow::Result<()> {
-
    let signer = &term::signer(profile)?;
-
    let mut patches = term::cob::patches_mut(profile, repository)?;
-
    patches.remove(patch_id, signer)?;
+
    let signer = term::signer(profile)?;
+
    let mut patches = term::cob::patches_mut(profile, repository, &signer)?;
+
    patches.remove(patch_id)?;

    Ok(())
}
modified crates/radicle-cli/src/commands/patch/edit.rs
@@ -4,7 +4,6 @@ use radicle::cob;
use radicle::cob::Title;
use radicle::cob::patch;
use radicle::crypto;
-
use radicle::node::device::Device;
use radicle::prelude::*;
use radicle::storage::git::Repository;

@@ -18,26 +17,28 @@ pub fn run(
    repository: &Repository,
) -> anyhow::Result<()> {
    let signer = term::signer(profile)?;
-
    let mut patches = term::cob::patches_mut(profile, repository)?;
+
    let mut patches = term::cob::patches_mut(profile, repository, &signer)?;
    let Ok(patch) = patches.get_mut(patch_id) else {
        anyhow::bail!("Patch `{patch_id}` not found");
    };
    let (title, description) = term::patch::get_edit_message(message, &patch)?;

    match revision_id {
-
        Some(id) => edit_revision(patch, id, title, description, &signer),
-
        None => edit_root(patch, title, description, &signer),
+
        Some(id) => edit_revision(patch, id, title, description),
+
        None => edit_root(patch, title, description),
    }
}

-
fn edit_root<G>(
-
    mut patch: patch::PatchMut<'_, '_, Repository, cob::cache::StoreWriter>,
+
fn edit_root<Signer>(
+
    mut patch: patch::PatchMut<'_, '_, '_, Repository, Signer, cob::cache::StoreWriter>,
    title: Title,
    description: String,
-
    signer: &Device<G>,
) -> anyhow::Result<()>
where
-
    G: crypto::signature::Signer<crypto::Signature>,
+
    Signer: crypto::signature::Signer<crypto::Signature>,
+
    Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
    Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
+
    Signer: crypto::signature::Verifier<crypto::Signature>,
{
    let title = if title.as_ref() != patch.title() {
        Some(title)
@@ -59,7 +60,7 @@ where
    let target = patch.target();
    let embeds = patch.embeds().to_owned();

-
    patch.transaction("Edit root", signer, |tx| {
+
    patch.transaction("Edit root", |tx| {
        if let Some(t) = title {
            tx.edit(t, target)?;
        }
@@ -72,15 +73,17 @@ where
    Ok(())
}

-
fn edit_revision<G>(
-
    mut patch: patch::PatchMut<'_, '_, Repository, cob::cache::StoreWriter>,
+
fn edit_revision<Signer>(
+
    mut patch: patch::PatchMut<'_, '_, '_, Repository, Signer, cob::cache::StoreWriter>,
    revision: patch::RevisionId,
    title: Title,
    description: String,
-
    signer: &Device<G>,
) -> anyhow::Result<()>
where
-
    G: crypto::signature::Signer<crypto::Signature>,
+
    Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
    Signer: crypto::signature::Signer<crypto::Signature>,
+
    Signer: crypto::signature::Signer<radicle::crypto::ssh::ExtendedSignature>,
+
    Signer: crypto::signature::Verifier<crypto::Signature>,
{
    let embeds = patch.embeds().to_owned();
    let mut message = title.to_string();
@@ -91,7 +94,7 @@ where
        message.push_str(&description);
        message
    };
-
    patch.transaction("Edit revision", signer, |tx| {
+
    patch.transaction("Edit revision", |tx| {
        tx.edit_revision(revision, message, embeds)?;
        Ok(())
    })?;
modified crates/radicle-cli/src/commands/patch/label.rs
@@ -12,7 +12,7 @@ pub fn run(
    repository: &Repository,
) -> anyhow::Result<()> {
    let signer = term::signer(profile)?;
-
    let mut patches = term::cob::patches_mut(profile, repository)?;
+
    let mut patches = term::cob::patches_mut(profile, repository, &signer)?;
    let Ok(mut patch) = patches.get_mut(patch_id) else {
        anyhow::bail!("Patch `{patch_id}` not found");
    };
@@ -22,6 +22,6 @@ pub fn run(
        .chain(add.iter())
        .cloned()
        .collect::<Vec<_>>();
-
    patch.label(labels, &signer)?;
+
    patch.label(labels)?;
    Ok(())
}
modified crates/radicle-cli/src/commands/patch/react.rs
@@ -18,7 +18,7 @@ pub fn run(
    profile: &Profile,
) -> anyhow::Result<()> {
    let signer = term::signer(profile)?;
-
    let mut patches = profile.patches_mut(repo)?;
+
    let mut patches = crate::terminal::cob::patches_mut(profile, repo, &signer)?;
    let revision_id = revision_id.resolve::<cob::EntryId>(&repo.backend)?;
    let ByRevision {
        id: patch_id,
@@ -29,7 +29,7 @@ pub fn run(
        .find_by_revision(&patch::RevisionId::from(revision_id))?
        .ok_or_else(|| anyhow!("Patch revision `{revision_id}` not found"))?;
    let mut patch = patch::PatchMut::new(patch_id, patch, &mut patches);
-
    patch.react(revision_id, reaction, None, active, &signer)?;
+
    patch.react(revision_id, reaction, None, active)?;

    Ok(())
}
modified crates/radicle-cli/src/commands/patch/ready.rs
@@ -10,15 +10,10 @@ pub fn run(
    repository: &Repository,
) -> anyhow::Result<bool> {
    let signer = term::signer(profile)?;
-
    let mut patches = term::cob::patches_mut(profile, repository)?;
+
    let mut patches = term::cob::patches_mut(profile, repository, &signer)?;
    let Ok(mut patch) = patches.get_mut(patch_id) else {
        anyhow::bail!("Patch `{patch_id}` not found");
    };

-
    if undo {
-
        patch.unready(&signer)
-
    } else {
-
        patch.ready(&signer)
-
    }
-
    .map_err(anyhow::Error::from)
+
    if undo { patch.unready() } else { patch.ready() }.map_err(anyhow::Error::from)
}
modified crates/radicle-cli/src/commands/patch/redact.rs
@@ -13,7 +13,7 @@ pub fn run(
    repository: &Repository,
) -> anyhow::Result<()> {
    let signer = &term::signer(profile)?;
-
    let mut patches = term::cob::patches_mut(profile, repository)?;
+
    let mut patches = term::cob::patches_mut(profile, repository, signer)?;

    let revision_id = revision_id.resolve::<Oid>(&repository.backend)?;
    let patch::ByRevision {
@@ -27,7 +27,7 @@ pub fn run(
        anyhow::bail!("Patch `{patch_id}` not found");
    };

-
    patch.redact(revision_id, signer)?;
+
    patch.redact(revision_id)?;

    Ok(())
}
modified crates/radicle-cli/src/commands/patch/resolve.rs
@@ -15,12 +15,12 @@ pub fn resolve(
    profile: &Profile,
) -> anyhow::Result<()> {
    let signer = term::signer(profile)?;
-
    let mut patches = term::cob::patches_mut(profile, repo)?;
+
    let mut patches = term::cob::patches_mut(profile, repo, &signer)?;
    let patch = patches
        .get(&patch_id)?
        .ok_or_else(|| anyhow!("Patch `{patch_id}` not found"))?;
    let mut patch = patch::PatchMut::new(patch_id, patch, &mut patches);
-
    patch.resolve_review_comment(review, comment, &signer)?;
+
    patch.resolve_review_comment(review, comment)?;
    Ok(())
}

@@ -32,11 +32,11 @@ pub fn unresolve(
    profile: &Profile,
) -> anyhow::Result<()> {
    let signer = term::signer(profile)?;
-
    let mut patches = term::cob::patches_mut(profile, repo)?;
+
    let mut patches = term::cob::patches_mut(profile, repo, &signer)?;
    let patch = patches
        .get(&patch_id)?
        .ok_or_else(|| anyhow!("Patch `{patch_id}` not found"))?;
    let mut patch = patch::PatchMut::new(patch_id, patch, &mut patches);
-
    patch.unresolve_review_comment(review, comment, &signer)?;
+
    patch.unresolve_review_comment(review, comment)?;
    Ok(())
}
modified crates/radicle-cli/src/commands/patch/review.rs
@@ -69,7 +69,7 @@ pub fn run(
        "couldn't load repository {} from local state",
        repository.id
    ))?;
-
    let mut patches = term::cob::patches_mut(profile, repository)?;
+
    let mut patches = term::cob::patches_mut(profile, repository, &signer)?;
    let mut patch = patches
        .get_mut(&patch_id)
        .context(format!("couldn't find patch {patch_id} locally"))?;
@@ -111,7 +111,7 @@ pub fn run(
            } else {
                Some(message)
            };
-
            patch.review(revision_id, verdict, message, vec![], &signer)?;
+
            patch.review(revision_id, verdict, message, vec![])?;

            match verdict {
                Some(Verdict::Accept) => {
modified crates/radicle-cli/src/commands/patch/review/builder.rs
@@ -648,7 +648,7 @@ impl<'a> ReviewBuilder<'a> {
        };
        let diff = self.diff(&brain.accepted, &tree, repo, opts)?;
        let drafts = DraftStore::new(self.repo, *signer.public_key());
-
        let mut patches = cob::patch::Cache::no_cache(&drafts)?;
+
        let mut patches = cob::patch::Cache::no_cache(&drafts, signer)?;
        let mut patch = patches.get_mut(&patch_id)?;
        let mut queue = ReviewQueue::from(diff);

@@ -668,7 +668,6 @@ impl<'a> ReviewBuilder<'a> {
                Some(Verdict::Reject),
                None,
                vec![],
-
                signer,
            )?
        };

@@ -719,7 +718,7 @@ impl<'a> ReviewBuilder<'a> {
                        let builder = CommentBuilder::new(revision.head(), path.to_path_buf());
                        let comments = builder.edit(hunk)?;

-
                        patch.transaction("Review comments", signer, |tx| {
+
                        patch.transaction("Review comments", |tx| {
                            for comment in comments {
                                tx.review_comment(
                                    review,
modified crates/radicle-cli/src/commands/patch/update.rs
@@ -20,7 +20,8 @@ pub fn run(
    let head_branch = try_branch(workdir.head()?)?;

    let (_, target_oid) = get_merge_target(repository, &head_branch)?;
-
    let mut patches = term::cob::patches_mut(profile, repository)?;
+
    let signer = term::signer(profile)?;
+
    let mut patches = term::cob::patches_mut(profile, repository, &signer)?;
    let Ok(mut patch) = patches.get_mut(&patch_id) else {
        anyhow::bail!("Patch `{patch_id}` not found");
    };
@@ -45,8 +46,7 @@ pub fn run(

    let (_, revision) = patch.latest();
    let message = term::patch::get_update_message(message, workdir, revision, &head_oid.into())?;
-
    let signer = term::signer(profile)?;
-
    let revision = patch.update(message, base_oid, head_oid, &signer)?;
+
    let revision = patch.update(message, base_oid, head_oid)?;

    term::print(revision);

modified crates/radicle-cli/src/commands/publish.rs
@@ -21,7 +21,8 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
    };

    let repo = profile.storage.repository_mut(rid)?;
-
    let mut identity = Identity::load_mut(&repo)?;
+
    let signer = profile.signer()?;
+
    let mut identity = Identity::load_mut(&repo, &signer)?;
    let doc = identity.doc();

    if doc.is_public() {
@@ -51,12 +52,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
    // SAFETY: the `Title` here is guaranteed to be nonempty and does not
    // contain `\n` or `\r`.
    #[allow(clippy::unwrap_used)]
-
    identity.update(
-
        cob::Title::new("Publish repository").unwrap(),
-
        "",
-
        &doc,
-
        &signer,
-
    )?;
+
    identity.update(cob::Title::new("Publish repository").unwrap(), "", &doc)?;
    repo.sign_refs(&signer)?;
    repo.set_identity_head()?;
    let validations = repo.validate()?;
modified crates/radicle-cli/src/terminal/cob.rs
@@ -3,6 +3,7 @@ use radicle::{
    cob::{
        self,
        cache::{MigrateCallback, MigrateProgress},
+
        store::access::{ReadOnly, WriteAs},
    },
    prelude::NodeId,
    profile,
@@ -58,47 +59,53 @@ pub mod migrate {
}

/// Return a read-only handle for the patches cache.
-
pub fn patches<'a, R>(
+
pub fn patches<'a, Repo>(
    profile: &Profile,
-
    repository: &'a R,
-
) -> Result<cob::patch::Cache<cob::patch::Patches<'a, R>, cob::cache::StoreReader>, anyhow::Error>
+
    repository: &'a Repo,
+
) -> Result<cob::patch::Cache<'a, Repo, ReadOnly, cob::cache::StoreReader>, anyhow::Error>
where
-
    R: ReadRepository + cob::Store<Namespace = NodeId>,
+
    Repo: ReadRepository + cob::Store<Namespace = NodeId>,
{
    profile.patches(repository).map_err(with_hint)
}

/// Return a read-write handle for the patches cache.
-
pub fn patches_mut<'a, R>(
+
/// Prefer this over [`radicle::profile::Home::patches_mut`],
+
/// to obtain an error hint in case migrations must be run.
+
pub fn patches_mut<'a, 'b, Repo, Signer>(
    profile: &Profile,
-
    repository: &'a R,
-
) -> Result<cob::patch::Cache<cob::patch::Patches<'a, R>, cob::cache::StoreWriter>, anyhow::Error>
+
    repository: &'a Repo,
+
    signer: &'b Signer,
+
) -> Result<cob::patch::Cache<'a, Repo, WriteAs<'b, Signer>, cob::cache::StoreWriter>, anyhow::Error>
where
-
    R: ReadRepository + cob::Store<Namespace = NodeId>,
+
    Repo: ReadRepository + cob::Store<Namespace = NodeId>,
{
-
    profile.patches_mut(repository).map_err(with_hint)
+
    profile.patches_mut(repository, signer).map_err(with_hint)
}

/// Return a read-only handle for the issues cache.
-
pub fn issues<'a, R>(
+
pub fn issues<'a, Repo>(
    profile: &Profile,
-
    repository: &'a R,
-
) -> Result<cob::issue::Cache<cob::issue::Issues<'a, R>, cob::cache::StoreReader>, anyhow::Error>
+
    repository: &'a Repo,
+
) -> Result<cob::issue::Cache<'a, Repo, ReadOnly, cob::cache::StoreReader>, anyhow::Error>
where
-
    R: ReadRepository + cob::Store<Namespace = NodeId>,
+
    Repo: ReadRepository + cob::Store<Namespace = NodeId>,
{
    profile.issues(repository).map_err(with_hint)
}

/// Return a read-write handle for the issues cache.
-
pub fn issues_mut<'a, R>(
+
/// Prefer this over [`radicle::profile::Home::issues_mut`],
+
/// to obtain an error hint in case migrations must be run.
+
pub fn issues_mut<'a, 'b, Repo, Signer>(
    profile: &Profile,
-
    repository: &'a R,
-
) -> Result<cob::issue::Cache<cob::issue::Issues<'a, R>, cob::cache::StoreWriter>, anyhow::Error>
+
    repository: &'a Repo,
+
    signer: &'b Signer,
+
) -> Result<cob::issue::Cache<'a, Repo, WriteAs<'b, Signer>, cob::cache::StoreWriter>, anyhow::Error>
where
-
    R: ReadRepository + cob::Store<Namespace = NodeId>,
+
    Repo: ReadRepository + cob::Store<Namespace = NodeId>,
{
-
    profile.issues_mut(repository).map_err(with_hint)
+
    profile.issues_mut(repository, signer).map_err(with_hint)
}

/// Adds a hint to the COB out-of-date database error.
modified crates/radicle-cli/tests/commands/cob.rs
@@ -2,6 +2,7 @@ use std::path::Path;

use crate::util::environment::Environment;
use crate::{program_reports_version, test};
+
use radicle::cob::store::access::{ReadOnly, WriteAs};
use radicle::node::Handle;
use radicle::test::fixtures;

@@ -135,7 +136,8 @@ fn test_cob_replication() {
        .unwrap();

    let bob_repo = radicle::storage::ReadStorage::repository(&bob.storage, rid).unwrap();
-
    let mut bob_issues = radicle::cob::issue::Issues::open(&bob_repo).unwrap();
+
    let mut bob_issues =
+
        radicle::cob::issue::Issues::open(&bob_repo, WriteAs::new(&bob.signer)).unwrap();
    let mut bob_cache = radicle::cob::cache::InMemory::default();
    let issue = bob_issues
        .create(
@@ -145,7 +147,6 @@ fn test_cob_replication() {
            &[],
            [],
            &mut bob_cache,
-
            &bob.signer,
        )
        .unwrap();
    log::debug!(target: "test", "Issue {} created", issue.id());
@@ -163,7 +164,7 @@ fn test_cob_replication() {
        .unwrap();

    let alice_repo = radicle::storage::ReadStorage::repository(&alice.storage, rid).unwrap();
-
    let alice_issues = radicle::cob::issue::Issues::open(&alice_repo).unwrap();
+
    let alice_issues = radicle::cob::issue::Issues::open(&alice_repo, ReadOnly).unwrap();
    let alice_issue = alice_issues.get(issue.id()).unwrap().unwrap();

    assert_eq!(alice_issue.title(), "Something's fishy");
@@ -192,7 +193,8 @@ fn test_cob_deletion() {
    bob.routes_to(&[(rid, alice.id)]);

    let alice_repo = radicle::storage::ReadStorage::repository(&alice.storage, rid).unwrap();
-
    let mut alice_issues = radicle::cob::issue::Cache::no_cache(&alice_repo).unwrap();
+
    let mut alice_issues =
+
        radicle::cob::issue::Cache::no_cache(&alice_repo, &alice.signer).unwrap();
    let issue = alice_issues
        .create(
            radicle::cob::Title::new("Something's fishy").unwrap(),
@@ -200,7 +202,6 @@ fn test_cob_deletion() {
            &[],
            &[],
            [],
-
            &alice.signer,
        )
        .unwrap();
    let issue_id = issue.id();
@@ -210,11 +211,12 @@ fn test_cob_deletion() {
        .unwrap();

    let bob_repo = radicle::storage::ReadStorage::repository(&bob.storage, rid).unwrap();
-
    let bob_issues = radicle::cob::issue::Issues::open(&bob_repo).unwrap();
+
    let bob_issues = radicle::cob::issue::Issues::open(&bob_repo, ReadOnly).unwrap();
    assert!(bob_issues.get(issue_id).unwrap().is_some());

-
    let mut alice_issues = radicle::cob::issue::Cache::no_cache(&alice_repo).unwrap();
-
    alice_issues.remove(issue_id, &alice.signer).unwrap();
+
    let mut alice_issues =
+
        radicle::cob::issue::Cache::no_cache(&alice_repo, &alice.signer).unwrap();
+
    alice_issues.remove(issue_id).unwrap();

    log::debug!(target: "test", "Removing issue..");

@@ -225,6 +227,6 @@ fn test_cob_deletion() {
        radicle::node::FetchResult::Success { .. }
    );
    let bob_repo = radicle::storage::ReadStorage::repository(&bob.storage, rid).unwrap();
-
    let bob_issues = radicle::cob::issue::Issues::open(&bob_repo).unwrap();
+
    let bob_issues = radicle::cob::issue::Issues::open(&bob_repo, ReadOnly).unwrap();
    assert!(bob_issues.get(issue_id).unwrap().is_none());
}
modified crates/radicle-node/src/test/node.rs
@@ -11,6 +11,10 @@ use std::{

use crossbeam_channel as chan;

+
use crate::node::NodeId;
+
use crate::node::device::Device;
+
use crate::storage::git::transport;
+
use crate::{Runtime, runtime, runtime::Handle, service};
use radicle::Storage;
use radicle::cob;
use radicle::cob::issue;
@@ -34,11 +38,6 @@ use radicle::rad;
use radicle::storage::{ReadStorage as _, RemoteRepository as _, SignRepository as _};
use radicle::test::fixtures;

-
use crate::node::NodeId;
-
use crate::node::device::Device;
-
use crate::storage::git::transport;
-
use crate::{Runtime, runtime, runtime::Handle, service};
-

/// A node that can be run.
pub struct Node<G> {
    pub id: NodeId,
@@ -360,13 +359,10 @@ impl<G: Signer<Signature> + cyphernet::Ecdh> NodeHandle<G> {
    }

    /// Create an [`issue::Issue`] in the `NodeHandle`'s storage.
-
    pub fn issue(&self, rid: RepoId, title: cob::Title, desc: &str) -> cob::ObjectId {
+
    pub fn issue(&mut self, rid: RepoId, title: cob::Title, desc: &str) -> cob::ObjectId {
        let repo = self.storage.repository(rid).unwrap();
-
        let mut issues = issue::Cache::no_cache(&repo).unwrap();
-
        *issues
-
            .create(title, desc, &[], &[], [], &self.signer)
-
            .unwrap()
-
            .id()
+
        let mut issues = issue::Cache::no_cache(&repo, &self.signer).unwrap();
+
        *issues.create(title, desc, &[], &[], []).unwrap().id()
    }

    /// Perform a commit to `refname` by generating a blob of random data to a
modified crates/radicle-node/src/test/peer.rs
@@ -134,7 +134,6 @@ impl Default for Config<MockSigner> {
impl<G: crypto::signature::Signer<crypto::Signature>> Peer<Storage, G> {
    pub fn project(&mut self, name: &str, description: &str) -> RepoId {
        radicle::storage::git::transport::local::register(self.storage().clone());
-

        let (repo, _) = fixtures::repository(self.tempdir.path().join(name));
        let (rid, _, _) = rad::init(
            &repo,
modified crates/radicle-node/src/tests.rs
@@ -1021,7 +1021,7 @@ fn test_refs_announcement_offline() {
    // Create an issue without telling the node.
    let repo = alice.storage().repository(rid).unwrap();
    let old_refs = RefsAt::new(&repo, alice.id).unwrap();
-
    let mut issues = radicle::issue::Cache::no_cache(&repo).unwrap();
+
    let mut issues = radicle::issue::Cache::no_cache(&repo, alice.signer()).unwrap();
    issues
        .create(
            cob::Title::new("Issue while offline!").unwrap(),
@@ -1029,7 +1029,6 @@ fn test_refs_announcement_offline() {
            &[],
            &[],
            [],
-
            alice.signer(),
        )
        .unwrap();
    let new_refs = RefsAt::new(&repo, alice.id).unwrap();
modified crates/radicle-node/src/tests/e2e.rs
@@ -2,6 +2,7 @@ use std::{collections::HashSet, thread, time};

use radicle::cob;
use radicle::cob::Title;
+
use radicle::cob::store::access::{ReadOnly, WriteAs};
use radicle_crypto::test::signer::MockSigner;
use test_log::test;

@@ -514,6 +515,7 @@ fn test_missing_remote() {
        .unwrap();
    assert!(result.is_success());
    log::debug!(target: "test", "Fetch complete with {}", bob.id);
+

    rad::fork_remote(acme, &alice.id, &carol, &bob.storage).unwrap();

    alice.issue(
@@ -935,7 +937,7 @@ fn test_non_fastforward_sigrefs() {
    let rid = bob.project("acme", "");

    let mut alice = alice.spawn();
-
    let bob = bob.spawn();
+
    let mut bob = bob.spawn();
    let mut eve = eve.spawn();

    alice.handle.seed(rid, Scope::All).unwrap();
@@ -1114,7 +1116,7 @@ fn test_outdated_sigrefs() {
        FetchResult::Success { .. }
    );
    let repo = alice.storage.repository(rid).unwrap();
-
    let issues = issue::Issues::open(&repo).unwrap();
+
    let issues = issue::Issues::open(&repo, WriteAs::new(&alice.signer)).unwrap();
    assert!(
        issues.get(&issue_id).unwrap().is_some(),
        "Alice did not fetch issue {issue_id}"
@@ -1348,12 +1350,14 @@ fn missing_delegate_default_branch() {
        .unwrap();
    assert!(bob.storage.contains(&rid).unwrap());

+
    let bob_key = *bob.signer.public_key();
+

    // Helper to assert that Bob's default branch is not in storage
    let assert_bobs_default_is_missing = |repo: &Repository| {
        let doc = repo.identity_doc().unwrap();
        let project = doc.project().unwrap();
        let default_branch = repo.reference(
-
            bob.signer.public_key(),
+
            &bob_key,
            &radicle::git::refs::branch(project.default_branch()),
        );
        assert!(matches!(
@@ -1365,7 +1369,7 @@ fn missing_delegate_default_branch() {
    // Add Bob as a delegate to the identity document
    {
        let repo = alice.storage.repository(rid).unwrap();
-
        let mut identity = Identity::load_mut(&repo).unwrap();
+
        let mut identity = Identity::load_mut(&repo, &alice.signer).unwrap();
        let doc = repo
            .identity_doc()
            .unwrap()
@@ -1375,7 +1379,7 @@ fn missing_delegate_default_branch() {
            })
            .unwrap();
        let rev = identity
-
            .update(Title::new("Add Bob").unwrap(), "", &doc, &alice.signer)
+
            .update(Title::new("Add Bob").unwrap(), "", &doc)
            .unwrap();
        repo.set_identity_head_to(rev).unwrap();

@@ -1735,6 +1739,7 @@ fn test_non_fastforward_identity_doc() {
        .handle
        .fetch(rid, alice.id, DEFAULT_TIMEOUT, None)
        .unwrap();
+

    // Alice pushes new references to her laptop
    let issue = alice_laptop.issue(
        rid,
@@ -1752,7 +1757,7 @@ fn test_non_fastforward_identity_doc() {
    // Alice updates the identity of the document to include her laptop
    let (prev, next) = {
        let repo = alice.storage.repository(rid).unwrap();
-
        let mut identity = Identity::load_mut(&repo).unwrap();
+
        let mut identity = Identity::load_mut(&repo, &alice.signer).unwrap();
        let prev = identity.current;
        let doc = repo
            .identity_doc()
@@ -1761,7 +1766,7 @@ fn test_non_fastforward_identity_doc() {
            .with_edits(|raw| raw.delegate(alice_laptop.id.into()))
            .unwrap();
        let rev = identity
-
            .update(Title::new("Add Laptop").unwrap(), "", &doc, &alice.signer)
+
            .update(Title::new("Add Laptop").unwrap(), "", &doc)
            .unwrap();
        repo.set_identity_head_to(rev).unwrap();
        (prev, rev)
@@ -1780,7 +1785,7 @@ fn test_non_fastforward_identity_doc() {
    assert!(matches!(result, FetchResult::Success { .. }));
    assert!(!has_issue(&bob, &issue));
    let repo = bob.storage.repository(rid).unwrap();
-
    let identity = Identity::load_mut(&repo).unwrap();
+
    let identity = Identity::load_mut(&repo, &bob.signer).unwrap();
    assert_eq!(identity.current, next);
    assert_eq!(identity.parent, Some(prev));

@@ -1793,7 +1798,7 @@ fn test_non_fastforward_identity_doc() {
    assert!(matches!(result, FetchResult::Success { .. }));
    assert!(has_issue(&bob, &issue));
    let repo = bob.storage.repository(rid).unwrap();
-
    let identity = Identity::load_mut(&repo).unwrap();
+
    let identity = Identity::load_mut(&repo, &bob.signer).unwrap();
    assert_eq!(identity.current, next);
    assert_eq!(identity.parent, Some(prev));
}
@@ -1917,7 +1922,7 @@ fn fetch_does_not_contain_rad_sigrefs_parent() {
        FetchResult::Success { .. }
    );
    let repo = bob.storage.repository(rid).unwrap();
-
    let issues = issue::Issues::open(&repo).unwrap();
+
    let issues = issue::Issues::open(&repo, ReadOnly).unwrap();
    assert!(
        issues.get(&issue_id).unwrap().is_some(),
        "Bob did not fetch issue {issue_id}"
modified crates/radicle-node/src/worker/fetch.rs
@@ -1,3 +1,4 @@
+
use radicle::cob::store::access::ReadOnly;
use radicle::identity::CanonicalRefs;
use radicle::identity::doc::CanonicalRefsError;
use radicle::storage::git::TempRepository;
@@ -272,8 +273,8 @@ where
    C: cob::cache::Update<cob::issue::Issue> + cob::cache::Update<cob::patch::Patch>,
    C: cob::cache::Remove<cob::issue::Issue> + cob::cache::Remove<cob::patch::Patch>,
{
-
    let mut issues = cob::store::Store::<cob::issue::Issue, _>::open(storage)?;
-
    let mut patches = cob::store::Store::<cob::patch::Patch, _>::open(storage)?;
+
    let mut issues = cob::store::Store::<cob::issue::Issue, _, _>::open(storage, ReadOnly)?;
+
    let mut patches = cob::store::Store::<cob::patch::Patch, _, _>::open(storage, ReadOnly)?;

    for update in refs {
        match update {
@@ -303,15 +304,15 @@ where
}

/// Update or remove a cache entry.
-
fn update_or_remove<R, C, T>(
-
    store: &mut cob::store::Store<T, R>,
+
fn update_or_remove<T, Repo, C>(
+
    store: &mut cob::store::Store<T, Repo, ReadOnly>,
    cache: &mut C,
    rid: &RepoId,
    tid: TypedId,
) -> Result<(), error::Cache>
where
-
    R: cob::Store + ReadRepository,
-
    T: cob::Evaluate<R> + cob::store::Cob + cob::store::CobWithType,
+
    T: cob::Evaluate<Repo> + cob::store::Cob + cob::store::CobWithType,
+
    Repo: cob::Store<Namespace = NodeId> + ReadRepository,
    C: cob::cache::Update<T> + cob::cache::Remove<T>,
{
    match store.get(&tid.id) {
modified crates/radicle-remote-helper/src/main.rs
@@ -28,6 +28,7 @@ use std::process;
use std::str::FromStr;
use std::{env, fmt};

+
use radicle::cob::store::access::{ReadOnly, WriteAs};
use thiserror::Error;

use radicle::prelude::NodeId;
@@ -458,10 +459,10 @@ pub(crate) fn warn(s: impl fmt::Display) {
}

/// Get the patch store.
-
pub(crate) fn patches<'a, R: ReadRepository + cob::Store<Namespace = NodeId>>(
+
pub(crate) fn patches<'a, Repo: ReadRepository + cob::Store<Namespace = NodeId>>(
    profile: &Profile,
-
    repo: &'a R,
-
) -> Result<cob::patch::Cache<cob::patch::Patches<'a, R>, cob::cache::StoreReader>, list::Error> {
+
    repo: &'a Repo,
+
) -> Result<cob::patch::Cache<'a, Repo, ReadOnly, cob::cache::StoreReader>, list::Error> {
    match profile.patches(repo) {
        Ok(patches) => Ok(patches),
        Err(err @ profile::Error::CobsCache(cob::cache::Error::OutOfDate)) => {
@@ -473,14 +474,17 @@ pub(crate) fn patches<'a, R: ReadRepository + cob::Store<Namespace = NodeId>>(
}

/// Get the mutable patch store.
-
pub(crate) fn patches_mut<'a>(
+
pub(crate) fn patches_mut<'a, 'b, Signer>(
    profile: &Profile,
    repo: &'a storage::git::Repository,
+
    signer: &'b Signer,
) -> Result<
-
    cob::patch::Cache<cob::patch::Patches<'a, storage::git::Repository>, cob::cache::StoreWriter>,
+
    cob::patch::Cache<'a, storage::git::Repository, WriteAs<'b, Signer>, cob::cache::StoreWriter>,
    push::Error,
-
> {
-
    match profile.patches_mut(repo) {
+
>
+
where
+
{
+
    match profile.patches_mut(repo, signer) {
        Ok(patches) => Ok(patches),
        Err(err @ profile::Error::CobsCache(cob::cache::Error::OutOfDate)) => {
            hint(cli::cob::MIGRATION_HINT);
modified crates/radicle-remote-helper/src/push.rs
@@ -8,9 +8,9 @@ use std::process::ExitStatus;
use std::str::FromStr;
use std::{assert_eq, io};

+
use radicle::cob::store::access::WriteAs;
use radicle::identity::crefs::GetCanonicalRefs as _;
use radicle::identity::doc::CanonicalRefsError;
-
use radicle::node::device::Device;
use thiserror::Error;

use radicle::Profile;
@@ -321,7 +321,8 @@ pub(super) fn run(
                    .map_err(Error::from)
            }
            Command::Push(git::fmt::refspec::Refspec { src, dst, force }) => {
-
                let patches = crate::patches_mut(profile, stored)?;
+
                let signer = profile.signer()?;
+
                let patches = crate::patches_mut(profile, stored, &signer)?;
                let action = PushAction::new(dst)?;

                match action {
@@ -332,7 +333,6 @@ pub(super) fn run(
                        &working,
                        stored,
                        patches,
-
                        &signer,
                        profile,
                        opts.clone(),
                        git,
@@ -560,24 +560,28 @@ impl<'a, G> Drop for TempPatchRef<'a, G> {
}

/// Open a new patch.
-
fn patch_open<G, S>(
+
fn patch_open<S, Signer>(
    head: &git::Oid,
    upstream: &Option<git::fmt::RefString>,
    nid: &NodeId,
    working: &git::raw::Repository,
    stored: &storage::git::Repository,
    mut patches: patch::Cache<
-
        patch::Patches<'_, storage::git::Repository>,
+
        '_,
+
        storage::git::Repository,
+
        WriteAs<'_, Signer>,
        cob::cache::StoreWriter,
    >,
-
    signer: &Device<G>,
    profile: &Profile,
    opts: Options,
    git: &S,
) -> Result<Option<ExplorerResource>, Error>
where
-
    G: crypto::signature::Signer<crypto::Signature>,
    S: GitService,
+
    Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
    Signer: crypto::signature::Signer<crypto::Signature>,
+
    Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
+
    Signer: crypto::signature::Verifier<crypto::Signature>,
{
    let temp = TempPatchRef::new(stored, head, nid, git);
    temp.push(head, opts.verbosity)?;
@@ -601,7 +605,6 @@ where
            base,
            *head,
            &[],
-
            signer,
        )
    } else {
        patches.create(
@@ -611,7 +614,6 @@ where
            base,
            *head,
            &[],
-
            signer,
        )
    }?;

@@ -698,7 +700,7 @@ where

/// Update an existing patch.
#[allow(clippy::too_many_arguments)]
-
fn patch_update<G, S>(
+
fn patch_update<S, Signer>(
    head: &git::Oid,
    dst: &git::fmt::Qualified,
    force: bool,
@@ -707,17 +709,22 @@ fn patch_update<G, S>(
    working: &git::raw::Repository,
    stored: &storage::git::Repository,
    mut patches: patch::Cache<
-
        patch::Patches<'_, storage::git::Repository>,
+
        '_,
+
        storage::git::Repository,
+
        WriteAs<'_, Signer>,
        cob::cache::StoreWriter,
    >,
-
    signer: &Device<G>,
+
    signer: &Signer,
    opts: Options,
    expected_refs: &[String],
    git: &S,
) -> Result<Option<ExplorerResource>, Error>
where
-
    G: crypto::signature::Signer<crypto::Signature>,
    S: GitService,
+
    Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
    Signer: crypto::signature::Signer<crypto::Signature>,
+
    Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
+
    Signer: crypto::signature::Verifier<crypto::Signature>,
{
    let Ok(Some(patch)) = patches.get(&patch_id) else {
        return Err(Error::NotFound(patch_id));
@@ -754,7 +761,7 @@ where
    )?;

    let mut patch_mut = patch::PatchMut::new(patch_id, patch, &mut patches);
-
    let revision = patch_mut.update(message, base, *head, signer)?;
+
    let revision = patch_mut.update(message, base, *head)?;
    let Some(revision) = patch_mut.revision(&revision).cloned() else {
        return Err(Error::RevisionNotFound(revision));
    };
@@ -787,7 +794,7 @@ where
    Ok(Some(ExplorerResource::Patch { id: patch_id }))
}

-
fn push<G, S>(
+
fn push<S, Signer>(
    src: &git::Oid,
    dst: &git::fmt::Qualified,
    force: bool,
@@ -795,17 +802,22 @@ fn push<G, S>(
    working: &git::raw::Repository,
    stored: &storage::git::Repository,
    mut patches: patch::Cache<
-
        patch::Patches<'_, storage::git::Repository>,
+
        '_,
+
        storage::git::Repository,
+
        WriteAs<'_, Signer>,
        cob::cache::StoreWriter,
    >,
-
    signer: &Device<G>,
+
    signer: &Signer,
    verbosity: Verbosity,
    expected_refs: &[String],
    git: &S,
) -> Result<Option<ExplorerResource>, Error>
where
-
    G: crypto::signature::Signer<crypto::Signature>,
    S: GitService,
+
    Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
    Signer: crypto::signature::Signer<crypto::Signature>,
+
    Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
+
    Signer: crypto::signature::Verifier<crypto::Signature>,
{
    let head = *src;
    let dst = dst.with_namespace(nid.into());
@@ -832,7 +844,7 @@ where
            let old = old.peel_to_commit()?.id();
            // Only delegates affect the merge state of the COB.
            if stored.delegates()?.contains(&nid.into()) {
-
                patch_revert_all(old.into(), head, &stored.backend, &mut patches, signer)?;
+
                patch_revert_all(old.into(), head, &stored.backend, &mut patches)?;
                patch_merge_all(old.into(), head, working, &mut patches, signer)?;
            }
        }
@@ -841,18 +853,20 @@ where
}

/// Revert all patches that are no longer included in the base branch.
-
fn patch_revert_all<G>(
+
fn patch_revert_all<Signer>(
    old: git::Oid,
    new: git::Oid,
    stored: &git::raw::Repository,
    patches: &mut patch::Cache<
-
        patch::Patches<'_, storage::git::Repository>,
+
        '_,
+
        storage::git::Repository,
+
        WriteAs<'_, Signer>,
        cob::cache::StoreWriter,
    >,
-
    _signer: &Device<G>,
) -> Result<(), Error>
where
-
    G: crypto::signature::Signer<crypto::Signature>,
+
    Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
    Signer: crypto::signature::Signer<crypto::Signature>,
{
    // Find all commits reachable from the old OID but not from the new OID.
    let mut revwalk = stored.revwalk()?;
@@ -906,18 +920,23 @@ where
}

/// Merge all patches that have been included in the base branch.
-
fn patch_merge_all<G>(
+
fn patch_merge_all<Signer>(
    old: git::Oid,
    new: git::Oid,
    working: &git::raw::Repository,
    patches: &mut patch::Cache<
-
        patch::Patches<'_, storage::git::Repository>,
+
        '_,
+
        storage::git::Repository,
+
        WriteAs<'_, Signer>,
        cob::cache::StoreWriter,
    >,
-
    signer: &Device<G>,
+
    signer: &Signer,
) -> Result<(), Error>
where
-
    G: crypto::signature::Signer<crypto::Signature>,
+
    Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
    Signer: crypto::signature::Signer<crypto::Signature>,
+
    Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
+
    Signer: crypto::signature::Verifier<crypto::Signature>,
{
    let mut revwalk = working.revwalk()?;
    revwalk.push_range(&format!("{old}..{new}"))?;
@@ -959,19 +978,22 @@ where
    Ok(())
}

-
fn patch_merge<C, G>(
-
    mut patch: patch::PatchMut<storage::git::Repository, C>,
+
fn patch_merge<Signer, C>(
+
    mut patch: patch::PatchMut<'_, '_, '_, storage::git::Repository, Signer, C>,
    revision: patch::RevisionId,
    commit: git::Oid,
    working: &git::raw::Repository,
-
    signer: &Device<G>,
+
    signer: &Signer,
) -> Result<(), Error>
where
+
    Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
    Signer: crypto::signature::Signer<crypto::Signature>,
+
    Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
+
    Signer: crypto::signature::Verifier<crypto::Signature>,
    C: cob::cache::Update<patch::Patch>,
-
    G: crypto::signature::Signer<crypto::Signature>,
{
    let (latest, _) = patch.latest();
-
    let merged = patch.merge(revision, commit, signer)?;
+
    let merged = patch.merge(revision, commit)?;

    if revision == latest {
        eprintln!(
modified crates/radicle/src/cob/cache.rs
@@ -112,9 +112,10 @@ pub enum Error {
    OutOfDate,
}

-
/// Read and write to the store.
+
/// Read from and write to the store.
pub type StoreWriter = Store<Write>;
-
/// Write to the store.
+

+
/// Read from the store.
pub type StoreReader = Store<Read>;

/// Read-only type witness.
modified crates/radicle/src/cob/identity.rs
@@ -3,15 +3,16 @@ use std::sync::LazyLock;
use std::{fmt, ops::Deref, str::FromStr};

use crypto::{PublicKey, Signature};
+
use nonempty::NonEmpty;
use radicle_cob::{Embed, ObjectId, TypeName};
use serde::{Deserialize, Serialize};
use thiserror::Error;

+
use crate::cob::store::access::WriteAs;
use crate::git;
use crate::git::Oid;
use crate::identity::doc::Doc;
use crate::node::NodeId;
-
use crate::node::device::Device;
use crate::storage;
use crate::{
    cob,
@@ -219,33 +220,34 @@ impl Identity {
        }
    }

-
    pub fn initialize<'a, R, G>(
+
    pub fn initialize<'a, 'b, Repo, Signer>(
        doc: &Doc,
-
        store: &'a R,
-
        signer: &Device<G>,
-
    ) -> Result<IdentityMut<'a, R>, cob::store::Error>
+
        store: &'a Repo,
+
        signer: &'b Signer,
+
    ) -> Result<IdentityMut<'a, 'b, Repo, Signer>, cob::store::Error>
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
        R: WriteRepository + cob::Store<Namespace = NodeId>,
+
        Repo: WriteRepository + cob::Store<Namespace = NodeId>,
+
        Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
        Signer: crypto::signature::Signer<crypto::Signature>,
+
        Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
+
        Signer: crypto::signature::Verifier<crypto::Signature>,
    {
-
        let mut store = cob::store::Store::open(store)?;
-
        let (id, identity) = Transaction::<Identity, _>::initial(
-
            "Initialize identity",
-
            &mut store,
-
            signer,
-
            |tx, repo| {
-
                tx.revision(
-
                    // SAFETY: "Initial revision" is a valid title
-
                    #[allow(clippy::unwrap_used)]
-
                    cob::Title::new("Initial revision").unwrap(),
-
                    "",
-
                    doc,
-
                    None,
-
                    repo,
-
                    signer,
-
                )
-
            },
-
        )?;
+
        let mut store = cob::store::Store::open(store, WriteAs::new(signer))?;
+

+
        #[allow(clippy::unwrap_used)]
+
        let title = cob::Title::new("Initial revision").unwrap();
+

+
        #[allow(deprecated)]
+
        let (actions, embeds) = {
+
            let repo = store.repo();
+
            let signer = store.signer();
+
            Transaction::new_revision(title, "", doc, None, repo, signer)?.into_inner()
+
        };
+

+
        let actions = NonEmpty::from_vec(actions)
+
            .expect("Transaction::initial: transaction must contain at least one action");
+

+
        let (id, identity) = store.create("Initialize identity", actions, embeds)?;

        Ok(IdentityMut {
            id,
@@ -254,10 +256,10 @@ impl Identity {
        })
    }

-
    pub fn get<R: ReadRepository + cob::Store>(
-
        object: &ObjectId,
-
        repo: &R,
-
    ) -> Result<Identity, store::Error> {
+
    pub fn get<Repo>(object: &ObjectId, repo: &Repo) -> Result<Identity, store::Error>
+
    where
+
        Repo: ReadRepository + cob::Store,
+
    {
        use cob::store::CobWithType;

        cob::get::<Self, _>(repo, Self::type_name(), object)
@@ -266,12 +268,17 @@ impl Identity {
    }

    /// Get a proposal mutably.
-
    pub fn get_mut<'a, R: WriteRepository + cob::Store<Namespace = NodeId>>(
+
    pub fn get_mut<'a, 'b, Repo, Signer>(
        id: &ObjectId,
-
        repo: &'a R,
-
    ) -> Result<IdentityMut<'a, R>, store::Error> {
+
        repo: &'a Repo,
+
        signer: &'b Signer,
+
    ) -> Result<IdentityMut<'a, 'b, Repo, Signer>, store::Error>
+
    where
+
        Repo: WriteRepository + cob::Store<Namespace = NodeId>,
+
        Signer: crypto::signature::Signer<crypto::Signature>,
+
    {
        let obj = Self::get(id, repo)?;
-
        let store = cob::store::Store::open(repo)?;
+
        let store = cob::store::Store::open(repo, WriteAs::new(signer))?;

        Ok(IdentityMut {
            id: *id,
@@ -287,13 +294,18 @@ impl Identity {
        Self::get(&oid, repo).map_err(RepositoryError::from)
    }

-
    pub fn load_mut<R: WriteRepository + cob::Store<Namespace = NodeId>>(
-
        repo: &R,
-
    ) -> Result<IdentityMut<'_, R>, RepositoryError> {
+
    pub fn load_mut<'a, 'b, Repo, Signer>(
+
        repo: &'a Repo,
+
        signer: &'b Signer,
+
    ) -> Result<IdentityMut<'a, 'b, Repo, Signer>, RepositoryError>
+
    where
+
        Repo: WriteRepository + cob::Store<Namespace = NodeId>,
+
        Signer: crypto::signature::Signer<crypto::Signature>,
+
    {
        let oid = repo.identity_root()?;
        let oid = ObjectId::from(oid);

-
        Self::get_mut(&oid, repo).map_err(RepositoryError::from)
+
        Self::get_mut(&oid, repo, signer).map_err(RepositoryError::from)
    }
}

@@ -885,43 +897,47 @@ impl<R: ReadRepository> store::Transaction<Identity, R> {
}

impl<R: WriteRepository> store::Transaction<Identity, R> {
-
    pub fn revision<G: crypto::signature::Signer<crypto::Signature>>(
-
        &mut self,
+
    pub fn new_revision<G: crypto::signature::Signer<crypto::Signature>>(
        title: cob::Title,
        description: impl ToString,
        doc: &Doc,
        parent: Option<RevisionId>,
        repo: &R,
-
        signer: &Device<G>,
-
    ) -> Result<(), store::Error> {
+
        signer: &G,
+
    ) -> Result<Self, store::Error> {
+
        let mut tx = Transaction::default();
+

        let (blob, bytes, signature) = doc.sign(signer).map_err(store::Error::Identity)?;
        // Store document blob in repository.
        let embed =
            Embed::<Uri>::store("radicle.json", &bytes, repo.raw()).map_err(store::Error::Git)?;
+

        debug_assert_eq!(embed.content, Uri::from(blob)); // Make sure we pre-computed the correct OID for the blob.

        // Identity document.
-
        self.embed([embed])?;
+
        tx.embed([embed])?;

        // Revision metadata.
-
        self.push(Action::Revision {
+
        tx.push(Action::Revision {
            title,
            description: description.to_string(),
            blob,
            parent,
            signature,
-
        })
+
        })?;
+

+
        Ok(tx)
    }
}

-
pub struct IdentityMut<'a, R> {
+
pub struct IdentityMut<'a, 'b, Repo, Signer> {
    pub id: ObjectId,

    identity: Identity,
-
    store: store::Store<'a, Identity, R>,
+
    store: store::Store<'a, Identity, Repo, WriteAs<'b, Signer>>,
}

-
impl<R> fmt::Debug for IdentityMut<'_, R> {
+
impl<Repo, Signer> fmt::Debug for IdentityMut<'_, '_, Repo, Signer> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("IdentityMut")
            .field("id", &self.id)
@@ -930,11 +946,16 @@ impl<R> fmt::Debug for IdentityMut<'_, R> {
    }
}

-
impl<R> IdentityMut<'_, R>
+
impl<Repo, Signer> IdentityMut<'_, '_, Repo, Signer>
where
-
    R: WriteRepository + cob::Store<Namespace = NodeId>,
+
    Repo: WriteRepository + cob::Store<Namespace = NodeId>,
+
    Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
    Signer: crypto::signature::Signer<crypto::Signature>,
+
    Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
+
    Signer: crypto::signature::Verifier<crypto::Signature>,
{
    /// Reload the identity data from storage.
+
    #[cfg(test)]
    pub fn reload(&mut self) -> Result<(), store::Error> {
        self.identity = self
            .store
@@ -944,20 +965,14 @@ where
        Ok(())
    }

-
    pub fn transaction<G, F>(
-
        &mut self,
-
        message: &str,
-
        signer: &Device<G>,
-
        operations: F,
-
    ) -> Result<EntryId, Error>
+
    pub fn transaction<F>(&mut self, message: &str, operations: F) -> Result<EntryId, Error>
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
        F: FnOnce(&mut Transaction<Identity, R>, &R) -> Result<(), store::Error>,
+
        F: FnOnce(&mut Transaction<Identity, Repo>, &Repo) -> Result<(), store::Error>,
    {
        let mut tx = Transaction::default();
        operations(&mut tx, self.store.as_ref())?;

-
        let (doc, commit) = tx.commit(message, self.id, &mut self.store, signer)?;
+
        let (doc, commit) = tx.commit(message, self.id, &mut self.store)?;
        self.identity = doc;

        Ok(commit)
@@ -965,70 +980,61 @@ where

    /// Update the identity by proposing a new revision.
    /// If the signer is the only delegate, the revision is accepted automatically.
-
    pub fn update<G>(
+
    pub fn update(
        &mut self,
        title: cob::Title,
        description: impl ToString,
        doc: &Doc,
-
        signer: &Device<G>,
-
    ) -> Result<RevisionId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
-
        let parent = self.current;
-
        let id = self.transaction("Propose revision", signer, |tx, repo| {
-
            tx.revision(title, description, doc, Some(parent), repo, signer)
-
        })?;
+
    ) -> Result<RevisionId, Error> {
+
        let parent = Some(self.current);
+

+
        #[allow(deprecated)]
+
        let tx = {
+
            let signer = self.store.signer();
+
            let repo = self.store.repo();
+
            Transaction::new_revision(title, description, doc, parent, repo, signer)?
+
        };
+
        let (doc, commit) = tx.commit("Propose revision", self.id, &mut self.store)?;
+
        self.identity = doc;

-
        Ok(id)
+
        Ok(commit)
    }

    /// Accept an active revision.
-
    pub fn accept<G>(&mut self, revision: &RevisionId, signer: &Device<G>) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
+
    pub fn accept(&mut self, revision: &RevisionId) -> Result<EntryId, Error> {
        let id = *revision;
        let revision = self.revision(revision).ok_or(Error::NotFound(id))?;
-
        let signature = revision.sign(signer)?;

-
        self.transaction("Accept revision", signer, |tx, _| tx.accept(id, signature))
+
        #[allow(deprecated)]
+
        let signature = revision.sign(self.store.signer())?;
+

+
        self.transaction("Accept revision", |tx, _| tx.accept(id, signature))
    }

    /// Reject an active revision.
-
    pub fn reject<G>(&mut self, revision: RevisionId, signer: &Device<G>) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
-
        self.transaction("Reject revision", signer, |tx, _| tx.reject(revision))
+
    pub fn reject(&mut self, revision: RevisionId) -> Result<EntryId, Error> {
+
        self.transaction("Reject revision", |tx, _| tx.reject(revision))
    }

    /// Redact a revision.
-
    pub fn redact<G>(&mut self, revision: RevisionId, signer: &Device<G>) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
-
        self.transaction("Redact revision", signer, |tx, _| tx.redact(revision))
+
    pub fn redact(&mut self, revision: RevisionId) -> Result<EntryId, Error> {
+
        self.transaction("Redact revision", |tx, _| tx.redact(revision))
    }

    /// Edit an active revision's title or description.
-
    pub fn edit<G>(
+
    pub fn edit(
        &mut self,
        revision: RevisionId,
        title: cob::Title,
        description: String,
-
        signer: &Device<G>,
-
    ) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
-
        self.transaction("Edit revision", signer, |tx, _| {
+
    ) -> Result<EntryId, Error> {
+
        self.transaction("Edit revision", |tx, _| {
            tx.edit(revision, title, description)
        })
    }
}

-
impl<R> Deref for IdentityMut<'_, R> {
+
impl<Repo, Signer> Deref for IdentityMut<'_, '_, Repo, Signer> {
    type Target = Identity;

    fn deref(&self) -> &Self::Target {
@@ -1076,6 +1082,7 @@ mod test {
    use crate::identity::Visibility;
    use crate::identity::did::Did;
    use crate::identity::doc::PayloadId;
+
    use crate::node::device::Device;
    use crate::rad;
    use crate::storage::ReadStorage;
    use crate::storage::git::Storage;
@@ -1101,7 +1108,7 @@ mod test {
        let NodeWithRepo { node, repo } = NodeWithRepo::default();
        let bob = Device::mock();
        let signer = &node.signer;
-
        let mut identity = Identity::load_mut(&*repo).unwrap();
+
        let mut identity = Identity::load_mut(&*repo, signer).unwrap();
        let mut doc = identity.doc().clone().edit();
        let title = Title::new("Identity update").unwrap();
        let description = "";
@@ -1111,12 +1118,7 @@ mod test {
        assert!(identity.current().is_accepted());
        // Using an identical document to the current one fails.
        identity
-
            .update(
-
                title.clone(),
-
                description,
-
                &doc.clone().verified().unwrap(),
-
                signer,
-
            )
+
            .update(title.clone(), description, &doc.clone().verified().unwrap())
            .unwrap_err();
        assert_eq!(identity.current, r0);

@@ -1129,12 +1131,7 @@ mod test {
        doc.delegate(bob.public_key().into());
        // The update should go through now.
        let r1 = identity
-
            .update(
-
                title.clone(),
-
                description,
-
                &doc.clone().verified().unwrap(),
-
                signer,
-
            )
+
            .update(title.clone(), description, &doc.clone().verified().unwrap())
            .unwrap();
        assert!(identity.revision(&r1).unwrap().is_accepted());
        assert_eq!(identity.current, r1);
@@ -1143,12 +1140,7 @@ mod test {
        // signs it.
        doc.visibility = Visibility::private([]);
        let r2 = identity
-
            .update(
-
                title.clone(),
-
                description,
-
                &doc.clone().verified().unwrap(),
-
                signer,
-
            )
+
            .update(title.clone(), description, &doc.clone().verified().unwrap())
            .unwrap();
        // R1 is still the head.
        assert_eq!(identity.current, r1);
@@ -1159,7 +1151,10 @@ mod test {
            &Visibility::Public
        );
        // Now let's add a signature on R2 from Bob.
-
        identity.accept(&r2, &bob).unwrap();
+
        let mut bob_identity = Identity::load_mut(&*repo, &bob).unwrap();
+
        bob_identity.accept(&r2).unwrap();
+

+
        identity.reload().unwrap();

        // R2 is now the head.
        assert_eq!(identity.current, r2);
@@ -1177,7 +1172,8 @@ mod test {
        let bob = Device::mock();
        let eve = Device::mock();
        let signer = &node.signer;
-
        let mut identity = Identity::load_mut(&*repo).unwrap();
+

+
        let mut identity = Identity::load_mut(&*repo, signer).unwrap();
        let mut doc = identity.doc().clone().edit();
        let description = "";

@@ -1188,7 +1184,6 @@ mod test {
                cob::Title::new("Identity update").unwrap(),
                description,
                &doc.clone().verified().unwrap(),
-
                signer,
            )
            .unwrap();
        assert_eq!(identity.current, r1);
@@ -1199,13 +1194,14 @@ mod test {
                cob::Title::new("Make private").unwrap(),
                description,
                &doc.clone().verified().unwrap(),
-
                &node.signer,
            )
            .unwrap();

+
        let mut bob_identity = Identity::load_mut(&*repo, &bob).unwrap();
+

        // 1/2 rejected means that we can never reach the required 2/2 votes.
-
        identity.reject(r2, &bob).unwrap();
-
        let r2 = identity.revision(&r2).unwrap();
+
        bob_identity.reject(r2).unwrap();
+
        let r2 = bob_identity.revision(&r2).unwrap();
        assert_eq!(r2.state, State::Rejected);

        // Now let's add another delegate.
@@ -1215,10 +1211,13 @@ mod test {
                cob::Title::new("Add Eve").unwrap(),
                description,
                &doc.clone().verified().unwrap(),
-
                &node.signer,
            )
            .unwrap();
-
        let _ = identity.accept(&r3, &bob).unwrap();
+

+
        bob_identity.reload().unwrap();
+
        let _ = bob_identity.accept(&r3).unwrap();
+

+
        identity.reload().unwrap();
        assert_eq!(identity.current, r3);

        doc.visibility = Visibility::Public;
@@ -1227,18 +1226,19 @@ mod test {
                cob::Title::new("Make public").unwrap(),
                description,
                &doc.verified().unwrap(),
-
                &node.signer,
            )
            .unwrap();

        // 1/3 rejected means that we can still reach the 2/3 required votes.
-
        identity.reject(r3, &bob).unwrap();
+
        bob_identity.reject(r3).unwrap();
        let r3 = identity.revision(&r3).unwrap().clone();
        assert_eq!(r3.state, State::Active); // Still active.

+
        let mut eve_identity = Identity::load_mut(&*repo, &eve).unwrap();
+

        // 2/3 rejected means that we can no longer reach the 2/3 required votes.
-
        identity.reject(r3.id, &eve).unwrap();
-
        let r3 = identity.revision(&r3.id).unwrap();
+
        eve_identity.reject(r3.id).unwrap();
+
        let r3 = eve_identity.revision(&r3.id).unwrap();
        assert_eq!(r3.state, State::Rejected);
    }

@@ -1248,7 +1248,7 @@ mod test {
        let alice = &network.alice;
        let bob = &network.bob;

-
        let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
+
        let mut alice_identity = Identity::load_mut(&*alice.repo, &alice.signer).unwrap();
        let mut alice_doc = alice_identity.doc().clone().edit();

        alice_doc.delegate(bob.signer.public_key().into());
@@ -1257,13 +1257,12 @@ mod test {
                cob::Title::new("Add Bob").unwrap(),
                "",
                &alice_doc.clone().verified().unwrap(),
-
                &alice.signer,
            )
            .unwrap();

        bob.repo.fetch(alice);

-
        let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
+
        let bob_identity = Identity::load(&*bob.repo).unwrap();
        let bob_doc = bob_identity.doc().clone();
        assert!(bob_doc.is_delegate(&bob.signer.public_key().into()));

@@ -1274,16 +1273,19 @@ mod test {
                cob::Title::new("Change visibility").unwrap(),
                "",
                &alice_doc.clone().clone().verified().unwrap(),
-
                &alice.signer,
            )
            .unwrap();
+

+
        let bob_identity_mut = Identity::load_mut(&*bob.repo, &bob.signer).unwrap();
+
        assert_eq!(*bob_identity_mut, bob_identity);
+
        let mut bob_identity = bob_identity_mut;
+

        // Bob makes the same change without knowing Alice already did.
        let b1 = bob_identity
            .update(
                cob::Title::new("Make private").unwrap(),
                "",
                &alice_doc.verified().unwrap(),
-
                &bob.signer,
            )
            .unwrap();

@@ -1301,7 +1303,7 @@ mod test {
        assert_eq!(bob_identity.revision(&b1).unwrap().state, State::Active);

        // Now Bob accepts Alice's proposal. This voids his own.
-
        bob_identity.accept(&a2, &bob.signer).unwrap();
+
        bob_identity.accept(&a2).unwrap();
        assert_eq!(bob_identity.current, a2);
        assert_eq!(bob_identity.revision(&a1).unwrap().state, State::Accepted);
        assert_eq!(bob_identity.revision(&a2).unwrap().state, State::Accepted);
@@ -1315,7 +1317,7 @@ mod test {
        let bob = &network.bob;
        let eve = &network.eve;

-
        let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
+
        let mut alice_identity = Identity::load_mut(&*alice.repo, &alice.signer).unwrap();
        let mut alice_doc = alice_identity.doc().clone().edit();

        alice_doc.delegate(bob.signer.public_key().into());
@@ -1325,7 +1327,6 @@ mod test {
                cob::Title::new("Add Bob").unwrap(),
                "Eh.",
                &alice_doc.clone().clone().verified().unwrap(),
-
                &alice.signer,
            )
            .unwrap();

@@ -1335,20 +1336,16 @@ mod test {
                cob::Title::new("Change visibility").unwrap(),
                "Eh.",
                &alice_doc.verified().unwrap(),
-
                &alice.signer,
            )
            .unwrap();

        bob.repo.fetch(alice);
-
        let a3 = cob::stable::with_advanced_timestamp(|| {
-
            alice_identity.redact(a2, &alice.signer).unwrap()
-
        });
+
        let a3 = cob::stable::with_advanced_timestamp(|| alice_identity.redact(a2).unwrap());
        assert!(alice_identity.revision(&a1).is_some());
        assert_eq!(alice_identity.timeline, vec![a0, a1, a2, a3]);

-
        let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
-
        let b1 =
-
            cob::stable::with_advanced_timestamp(|| bob_identity.accept(&a2, &bob.signer).unwrap());
+
        let mut bob_identity = Identity::load_mut(&*bob.repo, &bob.signer).unwrap();
+
        let b1 = cob::stable::with_advanced_timestamp(|| bob_identity.accept(&a2).unwrap());

        assert_eq!(bob_identity.timeline, vec![a0, a1, a2, b1]);
        assert_eq!(bob_identity.revision(&a2).unwrap().state, State::Accepted);
@@ -1367,7 +1364,7 @@ mod test {
        let bob = &network.bob;
        let eve = &network.eve;

-
        let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
+
        let mut alice_identity = Identity::load_mut(&*alice.repo, &alice.signer).unwrap();
        let mut alice_doc = alice_identity.doc().clone().edit();

        alice_doc.delegate(bob.signer.public_key().into());
@@ -1378,7 +1375,6 @@ mod test {
                cob::Title::new("Add Bob and Eve").unwrap(),
                "Eh#!",
                &alice_doc.clone().verified().unwrap(),
-
                &alice.signer,
            )
            .unwrap();

@@ -1388,7 +1384,6 @@ mod test {
                cob::Title::new("Remove Eve").unwrap(),
                "",
                &alice_doc.verified().unwrap(),
-
                &alice.signer,
            )
            .unwrap();

@@ -1396,12 +1391,11 @@ mod test {
        bob.repo.fetch(alice);
        eve.repo.fetch(bob);

-
        let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
-
        let b1 =
-
            cob::stable::with_advanced_timestamp(|| bob_identity.accept(&a2, &bob.signer).unwrap());
+
        let mut bob_identity = Identity::load_mut(&*bob.repo, &bob.signer).unwrap();
+
        let b1 = cob::stable::with_advanced_timestamp(|| bob_identity.accept(&a2).unwrap());
        assert_eq!(bob_identity.current, a2);

-
        let mut eve_identity = Identity::load_mut(&*eve.repo).unwrap();
+
        let mut eve_identity = Identity::load_mut(&*eve.repo, &eve.signer).unwrap();
        let mut eve_doc = eve_identity.doc().clone().edit();
        eve_doc.visibility = Visibility::private([eve.signer.public_key().into()]);
        let e1 = cob::stable::with_advanced_timestamp(|| {
@@ -1410,7 +1404,6 @@ mod test {
                    cob::Title::new("Change visibility").unwrap(),
                    "",
                    &eve_doc.verified().unwrap(),
-
                    &eve.signer,
                )
                .unwrap()
        });
@@ -1443,7 +1436,7 @@ mod test {
        let bob = &network.bob;
        let eve = &network.eve;

-
        let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
+
        let mut alice_identity = Identity::load_mut(&*alice.repo, &alice.signer).unwrap();
        let mut alice_doc = alice_identity.doc().clone().edit();

        alice_doc.delegate(bob.signer.public_key().into());
@@ -1454,7 +1447,6 @@ mod test {
                cob::Title::new("Add Bob and Eve").unwrap(),
                "Eh!#",
                &alice_doc.clone().verified().unwrap(),
-
                &alice.signer,
            )
            .unwrap();

@@ -1464,7 +1456,6 @@ mod test {
                cob::Title::new("Change visibility").unwrap(),
                "",
                &alice_doc.verified().unwrap(),
-
                &alice.signer,
            )
            .unwrap();

@@ -1473,14 +1464,12 @@ mod test {
        eve.repo.fetch(bob);

        // Bob accepts alice's revision.
-
        let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
-
        let b1 =
-
            cob::stable::with_advanced_timestamp(|| bob_identity.accept(&a2, &bob.signer).unwrap());
+
        let mut bob_identity = Identity::load_mut(&*bob.repo, &bob.signer).unwrap();
+
        let b1 = cob::stable::with_advanced_timestamp(|| bob_identity.accept(&a2).unwrap());

        // Eve rejects the revision, not knowing.
-
        let mut eve_identity = Identity::load_mut(&*eve.repo).unwrap();
-
        let e1 =
-
            cob::stable::with_advanced_timestamp(|| eve_identity.reject(a2, &eve.signer).unwrap());
+
        let mut eve_identity = Identity::load_mut(&*eve.repo, &eve.signer).unwrap();
+
        let e1 = cob::stable::with_advanced_timestamp(|| eve_identity.reject(a2).unwrap());
        assert!(eve_identity.revision(&a2).unwrap().is_active());

        // Then she submits a new revision.
@@ -1491,7 +1480,6 @@ mod test {
                cob::Title::new("Change visibility").unwrap(),
                "",
                &eve_doc.verified().unwrap(),
-
                &eve.signer,
            )
            .unwrap();
        assert!(eve_identity.revision(&e2).unwrap().is_active());
@@ -1529,7 +1517,7 @@ mod test {
        let bob = &network.bob;
        let eve = &network.eve;

-
        let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
+
        let mut alice_identity = Identity::load_mut(&*alice.repo, &alice.signer).unwrap();
        let mut alice_doc = alice_identity.doc().clone().edit();

        alice.repo.fetch(bob);
@@ -1542,14 +1530,13 @@ mod test {
                cob::Title::new("Add Bob and Eve").unwrap(),
                "",
                &alice_doc.verified().unwrap(),
-
                &alice.signer,
            )
            .unwrap();

        bob.repo.fetch(alice);
        eve.repo.fetch(alice);

-
        let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
+
        let mut bob_identity = Identity::load_mut(&*bob.repo, &bob.signer).unwrap();
        let mut bob_doc = bob_identity.doc().clone().edit();
        assert!(bob_doc.is_delegate(&bob.signer.public_key().into()));

@@ -1568,14 +1555,14 @@ mod test {
                cob::Title::new("Change visibility #1").unwrap(),
                "",
                &bob_doc.verified().unwrap(),
-
                &bob.signer,
            )
            .unwrap();
+

        alice.repo.fetch(bob);
        eve.repo.fetch(bob);

        // In the meantime, Eve does the same thing on her side.
-
        let mut eve_identity = Identity::load_mut(&*eve.repo).unwrap();
+
        let mut eve_identity = Identity::load_mut(&*eve.repo, &eve.signer).unwrap();
        let mut eve_doc = eve_identity.doc().clone().edit();
        eve_doc.visibility = Visibility::private([]);
        let e1 = eve_identity
@@ -1583,18 +1570,16 @@ mod test {
                cob::Title::new("Change visibility #2").unwrap(),
                "Woops",
                &eve_doc.verified().unwrap(),
-
                &eve.signer,
            )
            .unwrap();
        assert_eq!(eve_identity.revisions().count(), 4);
        assert_eq!(eve_identity.revision(&e1).unwrap().state, State::Active);

        alice_identity.reload().unwrap();
-
        let a2 = cob::stable::with_advanced_timestamp(|| {
-
            alice_identity.accept(&b1, &alice.signer).unwrap()
-
        });
+
        let a2 = cob::stable::with_advanced_timestamp(|| alice_identity.accept(&b1).unwrap());

        eve.repo.fetch(alice);
+

        eve_identity.reload().unwrap();

        assert_eq!(eve_identity.timeline, vec![a0, a1, b1, e1, a2]);
@@ -1619,7 +1604,7 @@ mod test {
        rad::fork_remote(id, alice.public_key(), &eve, &storage).unwrap();

        let repo = storage.repository(id).unwrap();
-
        let mut identity = Identity::load_mut(&repo).unwrap();
+
        let mut identity = Identity::load_mut(&repo, &alice).unwrap();
        let doc = identity.doc().clone();
        let prj = doc.project().unwrap();
        let mut doc = doc.edit();
@@ -1633,7 +1618,6 @@ mod test {
                cob::Title::new("Update description").unwrap(),
                "",
                &doc.clone().verified().unwrap(),
-
                &alice,
            )
            .unwrap();

@@ -1645,7 +1629,6 @@ mod test {
                cob::Title::new("Add bob").unwrap(),
                "",
                &doc.clone().verified().unwrap(),
-
                &alice,
            )
            .unwrap();

@@ -1658,25 +1641,26 @@ mod test {
                cob::Title::new("Add eve").unwrap(),
                "",
                &doc.clone().verified().unwrap(),
-
                &alice,
            )
            .unwrap();
-
        identity.accept(&revision, &bob).unwrap();
+

+
        let mut bob_identity = Identity::load_mut(&repo, &bob).unwrap();
+
        bob_identity.accept(&revision).unwrap();

        // Update description again with signatures by Eve and Bob.
        let desc = prj.description().to_owned() + "?";
        let prj = prj.update(None, desc, None).unwrap();
        doc.payload.insert(PayloadId::project(), prj.into());
-

-
        let revision = identity
+
        let revision = bob_identity
            .update(
                cob::Title::new("Update description again").unwrap(),
                "Bob's repository",
                &doc.verified().unwrap(),
-
                &bob,
            )
            .unwrap();
-
        identity.accept(&revision, &eve).unwrap();
+

+
        let mut eve_identity = Identity::load_mut(&repo, &eve).unwrap();
+
        eve_identity.accept(&revision).unwrap();

        let identity: Identity = Identity::load(&repo).unwrap();
        let root = repo.identity_root().unwrap();
modified crates/radicle/src/cob/issue.rs
@@ -11,13 +11,13 @@ use thiserror::Error;
use crate::cob;
use crate::cob::common::{Author, Authorization, Label, Reaction, Timestamp, Uri};
use crate::cob::store::Transaction;
+
use crate::cob::store::access::WriteAs;
use crate::cob::store::{Cob, CobAction};
use crate::cob::thread::{Comment, CommentId, Thread};
use crate::cob::{ActorId, Embed, EntryId, ObjectId, TypeName, op, store};
use crate::cob::{TitleError, thread};
use crate::identity::doc::DocError;
use crate::node::NodeId;
-
use crate::node::device::Device;
use crate::prelude::{Did, Doc, ReadRepository, RepoId};
use crate::storage;
use crate::storage::{HasRepoId, RepositoryError, WriteRepository};
@@ -472,8 +472,10 @@ impl Issue {
    }
}

-
impl<'a, 'g, R, C> From<IssueMut<'a, 'g, R, C>> for (IssueId, Issue) {
-
    fn from(value: IssueMut<'a, 'g, R, C>) -> Self {
+
impl<'a, 'b, 'g, Repo, Signer, Cache> From<IssueMut<'a, 'b, 'g, Repo, Signer, Cache>>
+
    for (IssueId, Issue)
+
{
+
    fn from(value: IssueMut<'a, 'b, 'g, Repo, Signer, Cache>) -> Self {
        (value.id, value.issue)
    }
}
@@ -579,14 +581,14 @@ impl<R: ReadRepository> store::Transaction<Issue, R> {
    }
}

-
pub struct IssueMut<'a, 'g, R, C> {
+
pub struct IssueMut<'a, 'b, 'g, Repo, Signer, Cache> {
    id: ObjectId,
    issue: Issue,
-
    store: &'g mut Issues<'a, R>,
-
    cache: &'g mut C,
+
    store: &'g mut Issues<'a, Repo, WriteAs<'b, Signer>>,
+
    cache: &'g mut Cache,
}

-
impl<R, C> std::fmt::Debug for IssueMut<'_, '_, R, C> {
+
impl<Repo, Signer, Cache> std::fmt::Debug for IssueMut<'_, '_, '_, Repo, Signer, Cache> {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        f.debug_struct("IssueMut")
            .field("id", &self.id)
@@ -595,10 +597,14 @@ impl<R, C> std::fmt::Debug for IssueMut<'_, '_, R, C> {
    }
}

-
impl<R, C> IssueMut<'_, '_, R, C>
+
impl<Repo, Signer, Cache> IssueMut<'_, '_, '_, Repo, Signer, Cache>
where
-
    R: WriteRepository + cob::Store<Namespace = NodeId>,
-
    C: cob::cache::Update<Issue>,
+
    Repo: WriteRepository + cob::Store<Namespace = NodeId>,
+
    Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
    Signer: crypto::signature::Signer<crypto::Signature>,
+
    Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
+
    Signer: crypto::signature::Verifier<crypto::Signature>,
+
    Cache: cob::cache::Update<Issue>,
{
    /// Reload the issue data from storage.
    pub fn reload(&mut self) -> Result<(), store::Error> {
@@ -606,7 +612,6 @@ where
            .store
            .get(&self.id)?
            .ok_or_else(|| store::Error::NotFound(TYPENAME.clone(), self.id))?;
-

        Ok(())
    }

@@ -616,132 +621,89 @@ where
    }

    /// Assign one or more actors to an issue.
-
    pub fn assign<G>(
-
        &mut self,
-
        assignees: impl IntoIterator<Item = Did>,
-
        signer: &Device<G>,
-
    ) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
-
        self.transaction("Assign", signer, |tx| tx.assign(assignees))
+
    pub fn assign(&mut self, assignees: impl IntoIterator<Item = Did>) -> Result<EntryId, Error> {
+
        self.transaction("Assign", |tx| tx.assign(assignees))
    }

    /// Set the issue title.
-
    pub fn edit<G>(&mut self, title: cob::Title, signer: &Device<G>) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
-
        self.transaction("Edit", signer, |tx| tx.edit(title))
+
    pub fn edit(&mut self, title: cob::Title) -> Result<EntryId, Error> {
+
        self.transaction("Edit", |tx| tx.edit(title))
    }

    /// Set the issue description.
-
    pub fn edit_description<G>(
+
    pub fn edit_description(
        &mut self,
        description: impl ToString,
        embeds: impl IntoIterator<Item = Embed<Uri>>,
-
        signer: &Device<G>,
-
    ) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
-
        let (id, _) = self.root();
+
    ) -> Result<EntryId, Error> {
+
        let (id, _) = self.issue.root();
        let id = *id;
-
        self.transaction("Edit description", signer, |tx| {
+
        self.transaction("Edit description", |tx| {
            tx.edit_comment(id, description, embeds.into_iter().collect())
        })
    }

    /// Lifecycle an issue.
-
    pub fn lifecycle<G>(&mut self, state: State, signer: &Device<G>) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
-
        self.transaction("Lifecycle", signer, |tx| tx.lifecycle(state))
+
    pub fn lifecycle(&mut self, state: State) -> Result<EntryId, Error> {
+
        self.transaction("Lifecycle", |tx| tx.lifecycle(state))
    }

    /// Comment on an issue.
-
    pub fn comment<G, S>(
+
    pub fn comment(
        &mut self,
-
        body: S,
+
        body: impl ToString,
        reply_to: CommentId,
        embeds: impl IntoIterator<Item = Embed<Uri>>,
-
        signer: &Device<G>,
-
    ) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
        S: ToString,
-
    {
-
        self.transaction("Comment", signer, |tx| {
+
    ) -> Result<EntryId, Error> {
+
        self.transaction("Comment", |tx| {
            tx.comment(body, reply_to, embeds.into_iter().collect())
        })
    }

    /// Edit a comment.
-
    pub fn edit_comment<G, S>(
+
    pub fn edit_comment(
        &mut self,
        id: CommentId,
-
        body: S,
+
        body: impl ToString,
        embeds: impl IntoIterator<Item = Embed<Uri>>,
-
        signer: &Device<G>,
-
    ) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
        S: ToString,
-
    {
-
        self.transaction("Edit comment", signer, |tx| {
+
    ) -> Result<EntryId, Error> {
+
        self.transaction("Edit comment", |tx| {
            tx.edit_comment(id, body, embeds.into_iter().collect())
        })
    }

    /// Redact a comment.
-
    pub fn redact_comment<G>(&mut self, id: CommentId, signer: &Device<G>) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
-
        self.transaction("Redact comment", signer, |tx| tx.redact_comment(id))
+
    pub fn redact_comment(&mut self, id: CommentId) -> Result<EntryId, Error> {
+
        self.transaction("Redact comment", |tx| tx.redact_comment(id))
    }

    /// Label an issue.
-
    pub fn label<G>(
-
        &mut self,
-
        labels: impl IntoIterator<Item = Label>,
-
        signer: &Device<G>,
-
    ) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
-
        self.transaction("Label", signer, |tx| tx.label(labels))
+
    pub fn label(&mut self, labels: impl IntoIterator<Item = Label>) -> Result<EntryId, Error> {
+
        self.transaction("Label", |tx| tx.label(labels))
    }

    /// React to an issue comment.
-
    pub fn react<G>(
+
    pub fn react(
        &mut self,
        to: CommentId,
        reaction: Reaction,
        active: bool,
-
        signer: &Device<G>,
-
    ) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
-
        self.transaction("React", signer, |tx| tx.react(to, reaction, active))
+
    ) -> Result<EntryId, Error> {
+
        self.transaction("React", |tx| tx.react(to, reaction, active))
    }

-
    pub fn transaction<G, F>(
-
        &mut self,
-
        message: &str,
-
        signer: &Device<G>,
-
        operations: F,
-
    ) -> Result<EntryId, Error>
+
    pub fn transaction<F>(&mut self, message: &str, operations: F) -> Result<EntryId, Error>
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
        F: FnOnce(&mut Transaction<Issue, R>) -> Result<(), store::Error>,
+
        Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
        Signer: crypto::signature::Signer<crypto::Signature>,
+
        Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
+
        Signer: crypto::signature::Verifier<crypto::Signature>,
+
        F: FnOnce(&mut Transaction<Issue, Repo>) -> Result<(), store::Error>,
    {
        let mut tx = Transaction::default();
        operations(&mut tx)?;

-
        let (issue, commit) = tx.commit(message, self.id, &mut self.store.raw, signer)?;
+
        let (issue, commit) = tx.commit(message, self.id, &mut self.store.raw)?;
        self.cache
            .update(&self.store.as_ref().id(), &self.id, &issue)
            .map_err(|e| Error::CacheUpdate {
@@ -754,7 +716,7 @@ where
    }
}

-
impl<R, C> Deref for IssueMut<'_, '_, R, C> {
+
impl<Repo, Signer, Cache> Deref for IssueMut<'_, '_, '_, Repo, Signer, Cache> {
    type Target = Issue;

    fn deref(&self) -> &Self::Target {
@@ -762,24 +724,24 @@ impl<R, C> Deref for IssueMut<'_, '_, R, C> {
    }
}

-
pub struct Issues<'a, R> {
-
    raw: store::Store<'a, Issue, R>,
+
pub struct Issues<'a, Repo, Access> {
+
    raw: store::Store<'a, Issue, Repo, Access>,
}

-
impl<'a, R> Deref for Issues<'a, R> {
-
    type Target = store::Store<'a, Issue, R>;
+
impl<'a, Repo, Access> Deref for Issues<'a, Repo, Access> {
+
    type Target = store::Store<'a, Issue, Repo, Access>;

    fn deref(&self) -> &Self::Target {
        &self.raw
    }
}

-
impl<R> HasRepoId for Issues<'_, R>
+
impl<Repo, Access> HasRepoId for Issues<'_, Repo, Access>
where
-
    R: ReadRepository,
+
    Repo: HasRepoId,
{
    fn rid(&self) -> RepoId {
-
        self.raw.as_ref().id()
+
        self.raw.rid()
    }
}

@@ -798,39 +760,61 @@ impl IssueCounts {
    }
}

-
impl<'a, R> Issues<'a, R>
+
impl<'a, Repo, Access> Issues<'a, Repo, Access>
where
-
    R: ReadRepository + cob::Store<Namespace = NodeId>,
+
    Repo: ReadRepository + cob::Store<Namespace = NodeId>,
+
    Access: store::access::Access,
{
    /// Open an issues store.
-
    pub fn open(repository: &'a R) -> Result<Self, RepositoryError> {
+
    pub fn open(repository: &'a Repo, access: Access) -> Result<Self, RepositoryError> {
        let identity = repository.identity_head()?;
-
        let raw = store::Store::open(repository)?.identity(identity);
+
        let raw = store::Store::open(repository, access)?.identity(identity);

        Ok(Self { raw })
    }
}

-
impl<'a, R> Issues<'a, R>
+
impl<'a, 'b, Repo, Signer> Issues<'a, Repo, WriteAs<'b, Signer>>
where
-
    R: WriteRepository + cob::Store<Namespace = NodeId>,
+
    Repo: WriteRepository + cob::Store<Namespace = NodeId>,
+
    Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
    Signer: crypto::signature::Signer<crypto::Signature>,
+
    Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
+
    Signer: crypto::signature::Verifier<crypto::Signature>,
{
+
    /// Get an issue mutably.
+
    pub fn get_mut<'g, Cache>(
+
        &'g mut self,
+
        id: &ObjectId,
+
        cache: &'g mut Cache,
+
    ) -> Result<IssueMut<'a, 'b, 'g, Repo, Signer, Cache>, Error> {
+
        let issue = self
+
            .raw
+
            .get(id)?
+
            .ok_or_else(move || store::Error::NotFound(TYPENAME.clone(), *id))?;
+

+
        Ok(IssueMut {
+
            id: *id,
+
            issue,
+
            store: self,
+
            cache,
+
        })
+
    }
+

    /// Create a new issue.
-
    pub fn create<'g, G, C>(
+
    pub fn create<'g, Cache>(
        &'g mut self,
        title: cob::Title,
        description: impl ToString,
        labels: &[Label],
        assignees: &[Did],
        embeds: impl IntoIterator<Item = Embed<Uri>>,
-
        cache: &'g mut C,
-
        signer: &Device<G>,
-
    ) -> Result<IssueMut<'a, 'g, R, C>, Error>
+
        cache: &'g mut Cache,
+
    ) -> Result<IssueMut<'a, 'b, 'g, Repo, Signer, Cache>, Error>
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
        C: cob::cache::Update<Issue>,
+
        Cache: cob::cache::Update<Issue>,
    {
-
        let (id, issue) = Transaction::initial("Create issue", &mut self.raw, signer, |tx, _| {
+
        let (id, issue) = Transaction::initial("Create issue", &mut self.raw, |tx, _| {
            tx.thread(description, embeds)?;
            tx.edit(title)?;

@@ -855,43 +839,24 @@ where
    }

    /// Remove an issue.
-
    pub fn remove<C, G>(&self, id: &ObjectId, signer: &Device<G>) -> Result<(), store::Error>
+
    pub fn remove<C, G>(&mut self, id: &ObjectId) -> Result<(), store::Error>
    where
        C: cob::cache::Remove<Issue>,
-
        G: crypto::signature::Signer<crypto::Signature>,
    {
-
        self.raw.remove(id, signer)
+
        self.raw.remove(id)
    }
}

-
impl<'a, R> Issues<'a, R>
+
impl<'a, Repo, Access> Issues<'a, Repo, Access>
where
-
    R: ReadRepository + cob::Store,
+
    Repo: ReadRepository + cob::Store<Namespace = NodeId>,
+
    Access: store::access::Access,
{
    /// Get an issue.
    pub fn get(&self, id: &ObjectId) -> Result<Option<Issue>, store::Error> {
        self.raw.get(id)
    }

-
    /// Get an issue mutably.
-
    pub fn get_mut<'g, C>(
-
        &'g mut self,
-
        id: &ObjectId,
-
        cache: &'g mut C,
-
    ) -> Result<IssueMut<'a, 'g, R, C>, store::Error> {
-
        let issue = self
-
            .raw
-
            .get(id)?
-
            .ok_or_else(move || store::Error::NotFound(TYPENAME.clone(), *id))?;
-

-
        Ok(IssueMut {
-
            id: *id,
-
            issue,
-
            store: self,
-
            cache,
-
        })
-
    }
-

    /// Issues count by state.
    pub fn counts(&self) -> Result<IssueCounts, Error> {
        let all = self.all()?;
@@ -984,23 +949,24 @@ mod test {
    use crate::cob::{ActorId, Reaction, store::CobWithType};
    use crate::git::Oid;
    use crate::issue::cache::Issues as _;
+
    use crate::node::device::Device;
    use crate::test::arbitrary;
    use crate::{assert_matches, test};

    #[test]
    fn test_concurrency() {
        let t = test::setup::Network::default();
-
        let mut issues_alice = Cache::no_cache(&*t.alice.repo).unwrap();
-
        let mut bob_issues = Cache::no_cache(&*t.bob.repo).unwrap();
-
        let mut eve_issues = Cache::no_cache(&*t.eve.repo).unwrap();
-
        let mut issue_alice = issues_alice
+

+
        let mut alice_issues = Cache::no_cache(&*t.alice.repo, &t.alice.signer).unwrap();
+
        let mut bob_issues = Cache::no_cache(&*t.bob.repo, &t.bob.signer).unwrap();
+
        let mut eve_issues = Cache::no_cache(&*t.eve.repo, &t.eve.signer).unwrap();
+
        let mut issue_alice = alice_issues
            .create(
                cob::Title::new("Alice Issue").unwrap(),
                "Alice's comment",
                &[],
                &[],
                [],
-
                &t.alice.signer,
            )
            .unwrap();
        let id = *issue_alice.id();
@@ -1011,12 +977,8 @@ mod test {
        let mut issue_eve = eve_issues.get_mut(&id).unwrap();
        let mut issue_bob = bob_issues.get_mut(&id).unwrap();

-
        issue_bob
-
            .comment("Bob's reply", *id, vec![], &t.bob.signer)
-
            .unwrap();
-
        issue_alice
-
            .comment("Alice's reply", *id, vec![], &t.alice.signer)
-
            .unwrap();
+
        issue_bob.comment("Bob's reply", *id, vec![]).unwrap();
+
        issue_alice.comment("Alice's reply", *id, vec![]).unwrap();

        assert_eq!(issue_bob.comments().count(), 2);
        assert_eq!(issue_alice.comments().count(), 2);
@@ -1042,9 +1004,7 @@ mod test {

        t.eve.repo.fetch(&t.alice);

-
        let eve_reply = issue_eve
-
            .comment("Eve's reply", *id, vec![], &t.eve.signer)
-
            .unwrap();
+
        let eve_reply = issue_eve.comment("Eve's reply", *id, vec![]).unwrap();

        t.bob.repo.fetch(&t.eve);
        t.alice.repo.fetch(&t.eve);
@@ -1078,7 +1038,7 @@ mod test {
    #[test]
    fn test_issue_create_and_assign() {
        let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
-
        let mut issues = Cache::no_cache(&*repo).unwrap();
+
        let mut issues = Cache::no_cache(&*repo, &node.signer).unwrap();

        let assignee = Did::from(arbitrary::r#gen::<ActorId>(1));
        let assignee_two = Did::from(arbitrary::r#gen::<ActorId>(1));
@@ -1089,7 +1049,6 @@ mod test {
                &[],
                &[assignee],
                [],
-
                &node.signer,
            )
            .unwrap();

@@ -1101,9 +1060,7 @@ mod test {
        assert!(assignees.contains(&assignee));

        let mut issue = issues.get_mut(&id).unwrap();
-
        issue
-
            .assign([assignee, assignee_two], &node.signer)
-
            .unwrap();
+
        issue.assign([assignee, assignee_two]).unwrap();

        let id = issue.id;
        let issue = issues.get(&id).unwrap().unwrap();
@@ -1117,7 +1074,7 @@ mod test {
    #[test]
    fn test_issue_create_and_reassign() {
        let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
-
        let mut issues = Cache::no_cache(&*repo).unwrap();
+
        let mut issues = Cache::no_cache(&*repo, &node.signer).unwrap();

        let assignee = Did::from(arbitrary::r#gen::<ActorId>(1));
        let assignee_two = Did::from(arbitrary::r#gen::<ActorId>(1));
@@ -1128,12 +1085,11 @@ mod test {
                &[],
                &[assignee, assignee_two],
                [],
-
                &node.signer,
            )
            .unwrap();

-
        issue.assign([assignee_two], &node.signer).unwrap();
-
        issue.assign([assignee_two], &node.signer).unwrap();
+
        issue.assign([assignee_two]).unwrap();
+
        issue.assign([assignee_two]).unwrap();
        issue.reload().unwrap();

        let assignees: Vec<_> = issue.assignees().cloned().collect::<Vec<_>>();
@@ -1141,7 +1097,7 @@ mod test {
        assert_eq!(1, assignees.len());
        assert!(assignees.contains(&assignee_two));

-
        issue.assign([], &node.signer).unwrap();
+
        issue.assign([]).unwrap();
        issue.reload().unwrap();

        assert_eq!(0, issue.assignees().count());
@@ -1150,7 +1106,7 @@ mod test {
    #[test]
    fn test_issue_create_and_get() {
        let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
-
        let mut issues = Cache::no_cache(&*repo).unwrap();
+
        let mut issues = Cache::no_cache(&*repo, &node.signer).unwrap();
        let created = issues
            .create(
                cob::Title::new("My first issue").unwrap(),
@@ -1158,7 +1114,6 @@ mod test {
                &[],
                &[],
                [],
-
                &node.signer,
            )
            .unwrap();

@@ -1176,7 +1131,7 @@ mod test {
    #[test]
    fn test_issue_create_and_change_state() {
        let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
-
        let mut issues = Cache::no_cache(&*repo).unwrap();
+
        let mut issues = Cache::no_cache(&*repo, &node.signer).unwrap();
        let mut issue = issues
            .create(
                cob::Title::new("My first issue").unwrap(),
@@ -1184,17 +1139,13 @@ mod test {
                &[],
                &[],
                [],
-
                &node.signer,
            )
            .unwrap();

        issue
-
            .lifecycle(
-
                State::Closed {
-
                    reason: CloseReason::Other,
-
                },
-
                &node.signer,
-
            )
+
            .lifecycle(State::Closed {
+
                reason: CloseReason::Other,
+
            })
            .unwrap();

        let id = issue.id;
@@ -1207,7 +1158,7 @@ mod test {
            }
        );

-
        issue.lifecycle(State::Open, &node.signer).unwrap();
+
        issue.lifecycle(State::Open).unwrap();
        let issue = issues.get(&id).unwrap().unwrap();

        assert_eq!(*issue.state(), State::Open);
@@ -1216,7 +1167,7 @@ mod test {
    #[test]
    fn test_issue_create_and_unassign() {
        let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
-
        let mut issues = Cache::no_cache(&*repo).unwrap();
+
        let mut issues = Cache::no_cache(&*repo, &node.signer).unwrap();

        let assignee = Did::from(arbitrary::r#gen::<ActorId>(1));
        let assignee_two = Did::from(arbitrary::r#gen::<ActorId>(1));
@@ -1227,12 +1178,11 @@ mod test {
                &[],
                &[assignee, assignee_two],
                [],
-
                &node.signer,
            )
            .unwrap();
        assert_eq!(2, issue.assignees().count());

-
        issue.assign([assignee_two], &node.signer).unwrap();
+
        issue.assign([assignee_two]).unwrap();
        issue.reload().unwrap();

        let assignees: Vec<_> = issue.assignees().cloned().collect::<Vec<_>>();
@@ -1244,7 +1194,7 @@ mod test {
    #[test]
    fn test_issue_edit() {
        let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
-
        let mut issues = Cache::no_cache(&*repo).unwrap();
+
        let mut issues = Cache::no_cache(&*repo, &node.signer).unwrap();

        let mut issue = issues
            .create(
@@ -1253,13 +1203,10 @@ mod test {
                &[],
                &[],
                [],
-
                &node.signer,
            )
            .unwrap();

-
        issue
-
            .edit(cob::Title::new("Sorry typo").unwrap(), &node.signer)
-
            .unwrap();
+
        issue.edit(cob::Title::new("Sorry typo").unwrap()).unwrap();

        let id = issue.id;
        let issue = issues.get(&id).unwrap().unwrap();
@@ -1271,7 +1218,7 @@ mod test {
    #[test]
    fn test_issue_edit_description() {
        let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
-
        let mut issues = Cache::no_cache(&*repo).unwrap();
+
        let mut issues = Cache::no_cache(&*repo, &node.signer).unwrap();
        let mut issue = issues
            .create(
                cob::Title::new("My first issue").unwrap(),
@@ -1279,12 +1226,11 @@ mod test {
                &[],
                &[],
                [],
-
                &node.signer,
            )
            .unwrap();

        issue
-
            .edit_description("Bob Loblaw law blog", vec![], &node.signer)
+
            .edit_description("Bob Loblaw law blog", vec![])
            .unwrap();

        let id = issue.id;
@@ -1297,7 +1243,7 @@ mod test {
    #[test]
    fn test_issue_react() {
        let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
-
        let mut issues = Cache::no_cache(&*repo).unwrap();
+
        let mut issues = Cache::no_cache(&*repo, &node.signer).unwrap();
        let mut issue = issues
            .create(
                cob::Title::new("My first issue").unwrap(),
@@ -1305,14 +1251,13 @@ mod test {
                &[],
                &[],
                [],
-
                &node.signer,
            )
            .unwrap();

        let (comment, _) = issue.root();
        let comment = *comment;
        let reaction = Reaction::new('🥳').unwrap();
-
        issue.react(comment, reaction, true, &node.signer).unwrap();
+
        issue.react(comment, reaction, true).unwrap();

        let id = issue.id;
        let issue = issues.get(&id).unwrap().unwrap();
@@ -1327,7 +1272,7 @@ mod test {
    #[test]
    fn test_issue_reply() {
        let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
-
        let mut issues = Cache::no_cache(&*repo).unwrap();
+
        let mut issues = Cache::no_cache(&*repo, &node.signer).unwrap();
        let mut issue = issues
            .create(
                cob::Title::new("My first issue").unwrap(),
@@ -1335,18 +1280,13 @@ mod test {
                &[],
                &[],
                [],
-
                &node.signer,
            )
            .unwrap();
        let (root, _) = issue.root();
        let root = *root;

-
        let c1 = issue
-
            .comment("Hi hi hi.", root, vec![], &node.signer)
-
            .unwrap();
-
        let c2 = issue
-
            .comment("Ha ha ha.", root, vec![], &node.signer)
-
            .unwrap();
+
        let c1 = issue.comment("Hi hi hi.", root, vec![]).unwrap();
+
        let c2 = issue.comment("Ha ha ha.", root, vec![]).unwrap();

        let id = issue.id;
        let mut issue = issues.get_mut(&id).unwrap();
@@ -1356,14 +1296,10 @@ mod test {
        assert_eq!(reply1.body(), "Hi hi hi.");
        assert_eq!(reply2.body(), "Ha ha ha.");

-
        issue.comment("Re: Hi.", c1, vec![], &node.signer).unwrap();
-
        issue.comment("Re: Ha.", c2, vec![], &node.signer).unwrap();
-
        issue
-
            .comment("Re: Ha. Ha.", c2, vec![], &node.signer)
-
            .unwrap();
-
        issue
-
            .comment("Re: Ha. Ha. Ha.", c2, vec![], &node.signer)
-
            .unwrap();
+
        issue.comment("Re: Hi.", c1, vec![]).unwrap();
+
        issue.comment("Re: Ha.", c2, vec![]).unwrap();
+
        issue.comment("Re: Ha. Ha.", c2, vec![]).unwrap();
+
        issue.comment("Re: Ha. Ha. Ha.", c2, vec![]).unwrap();

        let issue = issues.get(&id).unwrap().unwrap();

@@ -1382,7 +1318,7 @@ mod test {
    #[test]
    fn test_issue_label() {
        let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
-
        let mut issues = Cache::no_cache(&*repo).unwrap();
+
        let mut issues = Cache::no_cache(&*repo, &node.signer).unwrap();
        let bug_label = Label::new("bug").unwrap();
        let ux_label = Label::new("ux").unwrap();
        let wontfix_label = Label::new("wontfix").unwrap();
@@ -1393,18 +1329,12 @@ mod test {
                std::slice::from_ref(&ux_label),
                &[],
                [],
-
                &node.signer,
            )
            .unwrap();

+
        issue.label([ux_label.clone(), bug_label.clone()]).unwrap();
        issue
-
            .label([ux_label.clone(), bug_label.clone()], &node.signer)
-
            .unwrap();
-
        issue
-
            .label(
-
                [ux_label.clone(), bug_label.clone(), wontfix_label.clone()],
-
                &node.signer,
-
            )
+
            .label([ux_label.clone(), bug_label.clone(), wontfix_label.clone()])
            .unwrap();

        let id = issue.id;
@@ -1420,7 +1350,7 @@ mod test {
    fn test_issue_comment() {
        let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
        let author = *node.signer.public_key();
-
        let mut issues = Cache::no_cache(&*repo).unwrap();
+
        let mut issues = Cache::no_cache(&*repo, &node.signer).unwrap();
        let mut issue = issues
            .create(
                cob::Title::new("My first issue").unwrap(),
@@ -1428,7 +1358,6 @@ mod test {
                &[],
                &[],
                [],
-
                &node.signer,
            )
            .unwrap();

@@ -1436,12 +1365,8 @@ mod test {
        let (c0, _) = issue.root();
        let c0 = *c0;

-
        issue
-
            .comment("Ho ho ho.", c0, vec![], &node.signer)
-
            .unwrap();
-
        issue
-
            .comment("Ha ha ha.", c0, vec![], &node.signer)
-
            .unwrap();
+
        issue.comment("Ho ho ho.", c0, vec![]).unwrap();
+
        issue.comment("Ha ha ha.", c0, vec![]).unwrap();

        let id = issue.id;
        let issue = issues.get(&id).unwrap().unwrap();
@@ -1460,7 +1385,7 @@ mod test {
    #[test]
    fn test_issue_comment_redact() {
        let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
-
        let mut issues = Cache::no_cache(&*repo).unwrap();
+
        let mut issues = Cache::no_cache(&*repo, &node.signer).unwrap();
        let mut issue = issues
            .create(
                cob::Title::new("My first issue").unwrap(),
@@ -1468,7 +1393,6 @@ mod test {
                &[],
                &[],
                [],
-
                &node.signer,
            )
            .unwrap();

@@ -1476,17 +1400,15 @@ mod test {
        let (c0, _) = issue.root();
        let c0 = *c0;

-
        let comment = issue
-
            .comment("Ho ho ho.", c0, vec![], &node.signer)
-
            .unwrap();
+
        let comment = issue.comment("Ho ho ho.", c0, vec![]).unwrap();
        issue.reload().unwrap();
        assert_eq!(issue.comments().count(), 2);

-
        issue.redact_comment(comment, &node.signer).unwrap();
+
        issue.redact_comment(comment).unwrap();
        assert_eq!(issue.comments().count(), 1);

        // Can't redact root comment.
-
        issue.redact_comment(*issue.id, &node.signer).unwrap_err();
+
        issue.redact_comment(*issue.id).unwrap_err();
    }

    #[test]
@@ -1508,36 +1430,15 @@ mod test {
    #[test]
    fn test_issue_all() {
        let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
-
        let mut issues = Cache::no_cache(&*repo).unwrap();
+
        let mut issues = Cache::no_cache(&*repo, &node.signer).unwrap();
        issues
-
            .create(
-
                cob::Title::new("First").unwrap(),
-
                "Blah",
-
                &[],
-
                &[],
-
                [],
-
                &node.signer,
-
            )
+
            .create(cob::Title::new("First").unwrap(), "Blah", &[], &[], [])
            .unwrap();
        issues
-
            .create(
-
                cob::Title::new("Second").unwrap(),
-
                "Blah",
-
                &[],
-
                &[],
-
                [],
-
                &node.signer,
-
            )
+
            .create(cob::Title::new("Second").unwrap(), "Blah", &[], &[], [])
            .unwrap();
        issues
-
            .create(
-
                cob::Title::new("Third").unwrap(),
-
                "Blah",
-
                &[],
-
                &[],
-
                [],
-
                &node.signer,
-
            )
+
            .create(cob::Title::new("Third").unwrap(), "Blah", &[], &[], [])
            .unwrap();

        let issues = issues
@@ -1557,7 +1458,7 @@ mod test {
    #[test]
    fn test_issue_multilines() {
        let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
-
        let mut issues = Cache::no_cache(&*repo).unwrap();
+
        let mut issues = Cache::no_cache(&*repo, &node.signer).unwrap();
        let created = issues
            .create(
                cob::Title::new("My first issue").unwrap(),
@@ -1565,7 +1466,6 @@ mod test {
                &[],
                &[],
                [],
-
                &node.signer,
            )
            .unwrap();

@@ -1583,7 +1483,7 @@ mod test {
    #[test]
    fn test_embeds() {
        let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
-
        let mut issues = Cache::no_cache(&*repo).unwrap();
+
        let mut issues = Cache::no_cache(&*repo, &node.signer).unwrap();

        let content1 = repo.backend.blob(b"<html>Hello World!</html>").unwrap();
        let content2 = repo.backend.blob(b"<html>Hello Radicle!</html>").unwrap();
@@ -1608,17 +1508,11 @@ mod test {
                &[],
                &[],
                [embed1.clone(), embed2.clone()],
-
                &node.signer,
            )
            .unwrap();

        issue
-
            .comment(
-
                "Here's a binary file",
-
                *issue.id,
-
                [embed3.clone()],
-
                &node.signer,
-
            )
+
            .comment("Here's a binary file", *issue.id, [embed3.clone()])
            .unwrap();

        issue.reload().unwrap();
@@ -1642,7 +1536,7 @@ mod test {
    #[test]
    fn test_embeds_edit() {
        let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
-
        let mut issues = Cache::no_cache(&*repo).unwrap();
+
        let mut issues = Cache::no_cache(&*repo, &node.signer).unwrap();

        let content1 = repo.backend.blob(b"<html>Hello World!</html>").unwrap();
        let content1_edited = repo.backend.blob(b"<html>Hello Radicle!</html>").unwrap();
@@ -1667,13 +1561,12 @@ mod test {
                &[],
                &[],
                [embed1, embed2],
-
                &node.signer,
            )
            .unwrap();

        issue.reload().unwrap();
        issue
-
            .edit_description("My first issue", [embed1_edited.clone()], &node.signer)
+
            .edit_description("My first issue", [embed1_edited.clone()])
            .unwrap();
        issue.reload().unwrap();

@@ -1691,7 +1584,7 @@ mod test {
    #[test]
    fn test_invalid_actions() {
        let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
-
        let mut issues = Cache::no_cache(&*repo).unwrap();
+
        let mut issues = Cache::no_cache(&*repo, &node.signer).unwrap();
        let mut issue = issues
            .create(
                cob::Title::new("My first issue").unwrap(),
@@ -1699,14 +1592,11 @@ mod test {
                &[],
                &[],
                [],
-
                &node.signer,
            )
            .unwrap();
        let missing = arbitrary::oid();

-
        issue
-
            .comment("Invalid", missing, [], &node.signer)
-
            .unwrap_err();
+
        issue.comment("Invalid", missing, []).unwrap_err();
        assert_eq!(issue.comments().count(), 1);
        issue.reload().unwrap();
        assert_eq!(issue.comments().count(), 1);
@@ -1725,7 +1615,7 @@ mod test {
    #[test]
    fn test_invalid_tx() {
        let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
-
        let mut issues = Cache::no_cache(&*repo).unwrap();
+
        let mut issues = Cache::no_cache(&*repo, &node.signer).unwrap();
        let mut issue = issues
            .create(
                cob::Title::new("My first issue").unwrap(),
@@ -1733,7 +1623,6 @@ mod test {
                &[],
                &[],
                [],
-
                &node.signer,
            )
            .unwrap();
        let missing = arbitrary::oid();
@@ -1742,7 +1631,7 @@ mod test {
        // Even creating it via a transaction will trigger an error.
        let mut tx = Transaction::<Issue, _>::default();
        tx.comment("Invalid comment", missing, vec![]).unwrap();
-
        tx.commit("Add comment", issue.id, &mut issue.store.raw, &node.signer)
+
        tx.commit("Add comment", issue.id, &mut issue.store.raw)
            .unwrap_err();

        issue.reload().unwrap();
@@ -1752,7 +1641,7 @@ mod test {
    #[test]
    fn test_invalid_tx_reference() {
        let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
-
        let mut issues = Cache::no_cache(&*repo).unwrap();
+
        let mut issues = Cache::no_cache(&*repo, &node.signer).unwrap();
        let issue = issues
            .create(
                cob::Title::new("My first issue").unwrap(),
@@ -1760,7 +1649,6 @@ mod test {
                &[],
                &[],
                [],
-
                &node.signer,
            )
            .unwrap();

@@ -1783,7 +1671,7 @@ mod test {
        let identity = repo.identity().unwrap().head();
        let missing = arbitrary::oid();
        let type_name = Issue::type_name().clone();
-
        let mut issues = Cache::no_cache(&*repo).unwrap();
+
        let mut issues = Cache::no_cache(&*repo, &node.signer).unwrap();
        let mut issue = issues
            .create(
                cob::Title::new("My first issue").unwrap(),
@@ -1791,7 +1679,6 @@ mod test {
                &[],
                &[],
                [],
-
                &node.signer,
            )
            .unwrap();

@@ -1848,9 +1735,7 @@ mod test {

        // Additionally, when adding a *valid* comment, it does not build upon the bad operation.
        issue.reload().unwrap();
-
        issue
-
            .comment("Valid comment", *issue.id, vec![], &node.signer)
-
            .unwrap();
+
        issue.comment("Valid comment", *issue.id, vec![]).unwrap();
        issue.reload().unwrap();
        assert_eq!(issue.comments().count(), 2);
        assert_eq!(issue.thread.timeline().count(), 2);
@@ -1864,10 +1749,13 @@ mod test {
        assert_eq!(cob.history.len(), 3);
        assert_eq!(cob.object.len(), 3);

-
        // If Eve now writes a valid comment via the `Issue` type, it will overwrite her invalid
+
        let mut eve_issues = Cache::no_cache(&*repo, &eve).unwrap();
+
        let mut eve_issue = eve_issues.get_mut(issue.id()).unwrap();
+

+
        // If Eve now writes a valid comment via the `IssueMut` type, it will overwrite her invalid
        // one, since it won't be loaded as a tip.
-
        issue
-
            .comment("Eve's comment", *issue.id, vec![], &eve)
+
        eve_issue
+
            .comment("Eve's comment", *issue.id, vec![])
            .unwrap();

        let cob = cob::get::<NonEmpty<cob::Entry>, _>(&*repo, &type_name, issue.id())
@@ -1876,7 +1764,7 @@ mod test {

        // There are three nodes still, but they are all valid comments.
        // The invalid comment of Eve was replaced with a valid one.
-
        assert_eq!(issue.comments().count(), 3);
+
        assert_eq!(eve_issue.comments().count(), 3);
        assert_eq!(cob.history.len(), 3);
        assert_eq!(cob.object.len(), 3);
    }
modified crates/radicle/src/cob/issue/cache.rs
@@ -8,9 +8,9 @@ use crate::cob;
use crate::cob::cache;
use crate::cob::cache::{Remove, StoreReader, StoreWriter, Update};
use crate::cob::store;
+
use crate::cob::store::access::{ReadOnly, WriteAs};
use crate::cob::{Embed, Label, ObjectId, TypeName, Uri};
use crate::node::NodeId;
-
use crate::node::device::Device;
use crate::prelude::{Did, RepoId};
use crate::storage::{HasRepoId, ReadRepository, RepositoryError, SignRepository, WriteRepository};

@@ -77,39 +77,45 @@ impl<T> IssuesMut for T where T: Issues + Update<Issue> + Remove<Issue> {}
/// The `store` is used for the main storage when performing a
/// write-through. It is also used for identifying which `RepoId` is
/// being used for the `cache`.
-
pub struct Cache<R, C> {
-
    store: R,
+
pub struct Cache<'a, Repo, Access, C> {
+
    store: super::Issues<'a, Repo, Access>,
    cache: C,
}

-
impl<R, C> Cache<R, C> {
-
    pub fn new(store: R, cache: C) -> Self {
+
impl<'a, Repo, Access, C> Cache<'a, Repo, Access, C> {
+
    pub fn new(store: super::Issues<'a, Repo, Access>, cache: C) -> Self {
        Self { store, cache }
    }
+
}

-
    pub fn rid(&self) -> RepoId
-
    where
-
        R: HasRepoId,
-
    {
+
impl<'a, Repo, Access, C> HasRepoId for Cache<'a, Repo, Access, C>
+
where
+
    Repo: HasRepoId,
+
{
+
    fn rid(&self) -> RepoId {
        self.store.rid()
    }
}

-
impl<'a, R, C> Cache<super::Issues<'a, R>, C> {
+
impl<'a, 'b, Repo, Signer, C> Cache<'a, Repo, WriteAs<'b, Signer>, C>
+
where
+
    Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
    Signer: crypto::signature::Signer<crypto::Signature>,
+
    Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
+
    Signer: crypto::signature::Verifier<crypto::Signature>,
+
{
    /// Create a new [`Issue`] using the [`super::Issues`] as the
    /// main storage, and writing the update to the `cache`.
-
    pub fn create<'g, G>(
+
    pub fn create<'g>(
        &'g mut self,
        title: cob::Title,
        description: impl ToString,
        labels: &[Label],
        assignees: &[Did],
        embeds: impl IntoIterator<Item = Embed<Uri>>,
-
        signer: &Device<G>,
-
    ) -> Result<IssueMut<'a, 'g, R, C>, super::Error>
+
    ) -> Result<IssueMut<'a, 'b, 'g, Repo, Signer, C>, super::Error>
    where
-
        R: ReadRepository + WriteRepository + cob::Store<Namespace = NodeId>,
-
        G: crypto::signature::Signer<crypto::Signature>,
+
        Repo: ReadRepository + WriteRepository + cob::Store<Namespace = NodeId>,
        C: Update<Issue>,
    {
        self.store.create(
@@ -119,19 +125,17 @@ impl<'a, R, C> Cache<super::Issues<'a, R>, C> {
            assignees,
            embeds,
            &mut self.cache,
-
            signer,
        )
    }

    /// Remove the given `id` from the [`super::Issues`] storage, and
    /// removing the entry from the `cache`.
-
    pub fn remove<G>(&mut self, id: &IssueId, signer: &Device<G>) -> Result<(), super::Error>
+
    pub fn remove(&mut self, id: &IssueId) -> Result<(), super::Error>
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
        R: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
+
        Repo: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
        C: Remove<Issue>,
    {
-
        self.store.remove(id, signer)?;
+
        self.store.raw.remove(id)?;
        self.cache
            .remove(id)
            .map_err(|e| super::Error::CacheRemove {
@@ -140,12 +144,17 @@ impl<'a, R, C> Cache<super::Issues<'a, R>, C> {
            })?;
        Ok(())
    }
+
}

+
impl<'a, Repo, Access, C> Cache<'a, Repo, Access, C>
+
where
+
    Access: cob::store::access::Access,
+
{
    /// Read the given `id` from the [`super::Issues`] store and
    /// writing it to the `cache`.
    pub fn write(&mut self, id: &IssueId) -> Result<(), super::Error>
    where
-
        R: ReadRepository + cob::Store,
+
        Repo: ReadRepository + cob::Store<Namespace = NodeId>,
        C: Update<Issue>,
    {
        let issue = self
@@ -171,7 +180,7 @@ impl<'a, R, C> Cache<super::Issues<'a, R>, C> {
        on_issue: impl Fn(&Result<(IssueId, Issue), store::Error>, &cache::Progress) -> ControlFlow<()>,
    ) -> Result<(), super::Error>
    where
-
        R: ReadRepository + cob::Store,
+
        Repo: ReadRepository + cob::Store<Namespace = NodeId>,
        C: Update<Issue> + Remove<Issue>,
    {
        // Start by clearing the cache. This will get rid of issues that are cached but
@@ -198,14 +207,15 @@ impl<'a, R, C> Cache<super::Issues<'a, R>, C> {
    }
}

-
impl<'a, R> Cache<super::Issues<'a, R>, cache::NoCache>
+
impl<'a, 'b, Repo, Signer> Cache<'a, Repo, WriteAs<'b, Signer>, cache::NoCache>
where
-
    R: ReadRepository + cob::Store<Namespace = NodeId>,
+
    Repo: WriteRepository + cob::Store<Namespace = NodeId>,
+
    Signer: crypto::signature::Signer<crypto::Signature>,
{
    /// Get a `Cache` that does no write-through modifications and
    /// uses the [`super::Issues`] store for all reads and writes.
-
    pub fn no_cache(repository: &'a R) -> Result<Self, RepositoryError> {
-
        let store = super::Issues::open(repository)?;
+
    pub fn no_cache(repository: &'a Repo, signer: &'b Signer) -> Result<Self, RepositoryError> {
+
        let store = super::Issues::open(repository, WriteAs::new(signer))?;
        Ok(Self {
            store,
            cache: cache::NoCache,
@@ -216,7 +226,7 @@ where
    pub fn get_mut<'g>(
        &'g mut self,
        id: &ObjectId,
-
    ) -> Result<IssueMut<'a, 'g, R, cache::NoCache>, super::Error> {
+
    ) -> Result<IssueMut<'a, 'b, 'g, Repo, Signer, cache::NoCache>, super::Error> {
        let issue = self
            .store
            .get(id)?
@@ -231,28 +241,28 @@ where
    }
}

-
impl<R> Cache<R, StoreReader> {
-
    pub fn reader(store: R, cache: StoreReader) -> Self {
+
impl<'a, Repo> Cache<'a, Repo, ReadOnly, StoreReader> {
+
    pub fn reader(store: super::Issues<'a, Repo, ReadOnly>, cache: StoreReader) -> Self {
        Self { store, cache }
    }
}

-
impl<R> Cache<R, StoreWriter> {
-
    pub fn open(store: R, cache: StoreWriter) -> Self {
+
impl<'a, Repo, Access> Cache<'a, Repo, Access, StoreWriter> {
+
    pub fn open(store: super::Issues<'a, Repo, Access>, cache: StoreWriter) -> Self {
        Self { store, cache }
    }
}

-
impl<'a, R> Cache<super::Issues<'a, R>, StoreWriter>
+
impl<'a, 'b, Repo, Signer> Cache<'a, Repo, WriteAs<'b, Signer>, StoreWriter>
where
-
    R: ReadRepository + cob::Store,
+
    Repo: ReadRepository + cob::Store<Namespace = NodeId>,
{
    /// Get the [`IssueMut`], identified by `id`, using the
    /// `StoreWriter` for retrieving the `Issue`.
    pub fn get_mut<'g>(
        &'g mut self,
        id: &ObjectId,
-
    ) -> Result<IssueMut<'a, 'g, R, StoreWriter>, Error> {
+
    ) -> Result<IssueMut<'a, 'b, 'g, Repo, Signer, StoreWriter>, Error> {
        let issue = Issues::get(self, id)?
            .ok_or_else(move || Error::NotFound(super::TYPENAME.clone(), *id))?;

@@ -265,7 +275,7 @@ where
    }
}

-
impl<R, C> cache::Update<Issue> for Cache<R, C>
+
impl<'a, Repo, Access, C> cache::Update<Issue> for Cache<'a, Repo, Access, C>
where
    C: cache::Update<Issue>,
{
@@ -282,7 +292,7 @@ where
    }
}

-
impl<R, C> cache::Remove<Issue> for Cache<R, C>
+
impl<'a, Repo, Access, C> cache::Remove<Issue> for Cache<'a, Repo, Access, C>
where
    C: cache::Remove<Issue>,
{
@@ -377,9 +387,10 @@ impl Iterator for NoCacheIter<'_> {
    }
}

-
impl<R> Issues for Cache<super::Issues<'_, R>, cache::NoCache>
+
impl<'a, Repo, Access> Issues for Cache<'a, Repo, Access, cache::NoCache>
where
-
    R: ReadRepository + cob::Store,
+
    Repo: ReadRepository + cob::Store<Namespace = NodeId>,
+
    Access: store::access::Access,
{
    type Error = super::Error;
    type Iter<'b>
@@ -459,9 +470,9 @@ impl Iterator for IssuesIter<'_> {
    }
}

-
impl<R> Issues for Cache<R, StoreWriter>
+
impl<'a, Repo, Access> Issues for Cache<'a, Repo, Access, StoreWriter>
where
-
    R: HasRepoId,
+
    Repo: HasRepoId,
{
    type Error = Error;
    type Iter<'b>
@@ -486,9 +497,9 @@ where
    }
}

-
impl<R> Issues for Cache<R, StoreReader>
+
impl<'a, Repo, Access> Issues for Cache<'a, Repo, Access, StoreReader>
where
-
    R: HasRepoId,
+
    Repo: HasRepoId,
{
    type Error = Error;
    type Iter<'b>
@@ -618,14 +629,17 @@ mod tests {

    use crate::cob::cache::{Store, Update, Write};
    use crate::cob::migrate;
+
    use crate::cob::store::access::ReadOnly;
    use crate::cob::thread::Thread;
    use crate::issue::{CloseReason, Issue, IssueCounts, IssueId, State};
+
    use crate::storage::HasRepoId as _;
    use crate::test::arbitrary;
    use crate::test::storage::MockRepository;

    use super::{Cache, Issues};

-
    fn memory(store: MockRepository) -> Cache<MockRepository, Store<Write>> {
+
    fn memory<'a>(store: &'a MockRepository) -> Cache<'a, MockRepository, ReadOnly, Store<Write>> {
+
        let store = super::super::Issues::open(store, ReadOnly).unwrap();
        let cache = Store::<Write>::memory()
            .unwrap()
            .with_migrations(migrate::ignore)
@@ -636,7 +650,7 @@ mod tests {
    #[test]
    fn test_is_empty() {
        let repo = arbitrary::r#gen::<MockRepository>(1);
-
        let mut cache = memory(repo);
+
        let mut cache = memory(&repo);
        assert!(cache.is_empty().unwrap());

        let issue = Issue::new(Thread::default());
@@ -658,7 +672,7 @@ mod tests {
    #[test]
    fn test_counts() {
        let repo = arbitrary::r#gen::<MockRepository>(1);
-
        let mut cache = memory(repo);
+
        let mut cache = memory(&repo);
        let n_open = arbitrary::r#gen::<u8>(0);
        let n_closed = arbitrary::r#gen::<u8>(1);
        let open_ids = (0..n_open)
@@ -699,7 +713,7 @@ mod tests {
    #[test]
    fn test_get() {
        let repo = arbitrary::r#gen::<MockRepository>(1);
-
        let mut cache = memory(repo);
+
        let mut cache = memory(&repo);
        let ids = (0..arbitrary::r#gen::<u8>(1))
            .map(|_| IssueId::from(arbitrary::oid()))
            .collect::<BTreeSet<IssueId>>();
@@ -734,7 +748,7 @@ mod tests {
    #[test]
    fn test_list() {
        let repo = arbitrary::r#gen::<MockRepository>(1);
-
        let mut cache = memory(repo);
+
        let mut cache = memory(&repo);
        let ids = (0..arbitrary::r#gen::<u8>(1))
            .map(|_| IssueId::from(arbitrary::oid()))
            .collect::<BTreeSet<IssueId>>();
@@ -764,7 +778,7 @@ mod tests {
    #[test]
    fn test_list_by_status() {
        let repo = arbitrary::r#gen::<MockRepository>(1);
-
        let mut cache = memory(repo);
+
        let mut cache = memory(&repo);
        let ids = (0..arbitrary::r#gen::<u8>(1))
            .map(|_| IssueId::from(arbitrary::oid()))
            .collect::<BTreeSet<IssueId>>();
@@ -794,7 +808,7 @@ mod tests {
    #[test]
    fn test_remove() {
        let repo = arbitrary::r#gen::<MockRepository>(1);
-
        let mut cache = memory(repo);
+
        let mut cache = memory(&repo);
        let ids = (0..arbitrary::r#gen::<u8>(1))
            .map(|_| IssueId::from(arbitrary::oid()))
            .collect::<BTreeSet<IssueId>>();
modified crates/radicle/src/cob/patch.rs
@@ -21,6 +21,7 @@ use thiserror::Error;
use crate::cob;
use crate::cob::common::{Author, Authorization, CodeLocation, Label, Reaction, Timestamp};
use crate::cob::store::Transaction;
+
use crate::cob::store::access::WriteAs;
use crate::cob::store::{Cob, CobAction};
use crate::cob::thread;
use crate::cob::thread::Thread;
@@ -30,7 +31,6 @@ use crate::crypto::PublicKey;
use crate::git;
use crate::identity::PayloadError;
use crate::identity::doc::{DocAt, DocError};
-
use crate::node::device::Device;
use crate::prelude::*;
use crate::storage;

@@ -368,15 +368,17 @@ impl<R: WriteRepository> Merged<'_, R> {
    ///
    /// This removes Git refs relating to the patch, both in the working copy,
    /// and the stored copy; and updates `rad/sigrefs`.
-
    pub fn cleanup<G>(
+
    pub fn cleanup<Signer>(
        self,
        working: &git::raw::Repository,
-
        signer: &Device<G>,
+
        signer: &Signer,
    ) -> Result<(), storage::RepositoryError>
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
+
        Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
        Signer: crypto::signature::Signer<crypto::Signature>,
+
        Signer: crypto::signature::Verifier<crypto::Signature>,
    {
-
        let nid = signer.public_key();
+
        let nid = &signer.verifying_key();
        let stored_ref = git::refs::patch(&self.patch).with_namespace(nid.into());
        let working_ref = git::refs::workdir::patch_upstream(&self.patch);

@@ -2038,20 +2040,28 @@ impl<R: ReadRepository> store::Transaction<Patch, R> {
    }
}

-
pub struct PatchMut<'a, 'g, R, C> {
+
pub struct PatchMut<'a, 'b, 'g, Repo, Signer, Cache> {
    pub id: ObjectId,

    patch: Patch,
-
    store: &'g mut Patches<'a, R>,
-
    cache: &'g mut C,
+
    store: &'g mut Patches<'a, Repo, WriteAs<'b, Signer>>,
+
    cache: &'g mut Cache,
}

-
impl<'a, 'g, R, C> PatchMut<'a, 'g, R, C>
+
impl<'a, 'b, 'g, Repo, Signer, Update> PatchMut<'a, 'b, 'g, Repo, Signer, Update>
where
-
    C: cob::cache::Update<Patch>,
-
    R: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
+
    Repo: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
+
    Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
    Signer: crypto::signature::Signer<crypto::Signature>,
+
    Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
+
    Signer: crypto::signature::Verifier<crypto::Signature>,
+
    Update: cob::cache::Update<Patch>,
{
-
    pub fn new(id: ObjectId, patch: Patch, cache: &'g mut Cache<Patches<'a, R>, C>) -> Self {
+
    pub fn new(
+
        id: ObjectId,
+
        patch: Patch,
+
        cache: &'g mut Cache<'a, Repo, WriteAs<'b, Signer>, Update>,
+
    ) -> Self {
        Self {
            id,
            patch,
@@ -2074,20 +2084,14 @@ where
        Ok(())
    }

-
    pub fn transaction<G, F>(
-
        &mut self,
-
        message: &str,
-
        signer: &Device<G>,
-
        operations: F,
-
    ) -> Result<EntryId, Error>
+
    pub fn transaction<F>(&mut self, message: &str, operations: F) -> Result<EntryId, Error>
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
        F: FnOnce(&mut Transaction<Patch, R>) -> Result<(), store::Error>,
+
        F: FnOnce(&mut Transaction<Patch, Repo>) -> Result<(), store::Error>,
    {
        let mut tx = Transaction::default();
        operations(&mut tx)?;

-
        let (patch, commit) = tx.commit(message, self.id, &mut self.store.raw, signer)?;
+
        let (patch, commit) = tx.commit(message, self.id, &mut self.store.raw)?;
        self.cache
            .update(&self.store.as_ref().id(), &self.id, &patch)
            .map_err(|e| Error::CacheUpdate {
@@ -2100,73 +2104,46 @@ where
    }

    /// Edit patch metadata.
-
    pub fn edit<G, S>(
-
        &mut self,
-
        title: cob::Title,
-
        target: MergeTarget,
-
        signer: &Device<G>,
-
    ) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
        S: ToString,
-
    {
-
        self.transaction("Edit", signer, |tx| tx.edit(title, target))
+
    pub fn edit(&mut self, title: cob::Title, target: MergeTarget) -> Result<EntryId, Error> {
+
        self.transaction("Edit", |tx| tx.edit(title, target))
    }

    /// Edit revision metadata.
-
    pub fn edit_revision<G, S>(
+
    pub fn edit_revision(
        &mut self,
        revision: RevisionId,
-
        description: S,
+
        description: impl ToString,
        embeds: impl IntoIterator<Item = Embed<Uri>>,
-
        signer: &Device<G>,
-
    ) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
        S: ToString,
-
    {
-
        self.transaction("Edit revision", signer, |tx| {
+
    ) -> Result<EntryId, Error> {
+
        self.transaction("Edit revision", |tx| {
            tx.edit_revision(revision, description, embeds.into_iter().collect())
        })
    }

    /// Redact a revision.
-
    pub fn redact<G>(&mut self, revision: RevisionId, signer: &Device<G>) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
-
        self.transaction("Redact revision", signer, |tx| tx.redact(revision))
+
    pub fn redact(&mut self, revision: RevisionId) -> Result<EntryId, Error> {
+
        self.transaction("Redact revision", |tx| tx.redact(revision))
    }

    /// Create a thread on a patch revision.
-
    pub fn thread<G, S>(
+
    pub fn thread(
        &mut self,
        revision: RevisionId,
-
        body: S,
-
        signer: &Device<G>,
-
    ) -> Result<CommentId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
        S: ToString,
-
    {
-
        self.transaction("Create thread", signer, |tx| tx.thread(revision, body))
+
        body: impl ToString,
+
    ) -> Result<CommentId, Error> {
+
        self.transaction("Create thread", |tx| tx.thread(revision, body))
    }

    /// Comment on a patch revision.
-
    pub fn comment<G, S>(
+
    pub fn comment(
        &mut self,
        revision: RevisionId,
-
        body: S,
+
        body: impl ToString,
        reply_to: Option<CommentId>,
        location: Option<CodeLocation>,
        embeds: impl IntoIterator<Item = Embed<Uri>>,
-
        signer: &Device<G>,
-
    ) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
        S: ToString,
-
    {
-
        self.transaction("Comment", signer, |tx| {
+
    ) -> Result<EntryId, Error> {
+
        self.transaction("Comment", |tx| {
            tx.comment(
                revision,
                body,
@@ -2178,87 +2155,61 @@ where
    }

    /// React on a patch revision.
-
    pub fn react<G>(
+
    pub fn react(
        &mut self,
        revision: RevisionId,
        reaction: Reaction,
        location: Option<CodeLocation>,
        active: bool,
-
        signer: &Device<G>,
-
    ) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
-
        self.transaction("React", signer, |tx| {
-
            tx.react(revision, reaction, location, active)
-
        })
+
    ) -> Result<EntryId, Error> {
+
        self.transaction("React", |tx| tx.react(revision, reaction, location, active))
    }

    /// Edit a comment on a patch revision.
-
    pub fn comment_edit<G, S>(
+
    pub fn comment_edit(
        &mut self,
        revision: RevisionId,
        comment: CommentId,
-
        body: S,
+
        body: impl ToString,
        embeds: impl IntoIterator<Item = Embed<Uri>>,
-
        signer: &Device<G>,
-
    ) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
        S: ToString,
-
    {
-
        self.transaction("Edit comment", signer, |tx| {
+
    ) -> Result<EntryId, Error> {
+
        self.transaction("Edit comment", |tx| {
            tx.comment_edit(revision, comment, body, embeds.into_iter().collect())
        })
    }

    /// React to a comment on a patch revision.
-
    pub fn comment_react<G>(
+
    pub fn comment_react(
        &mut self,
        revision: RevisionId,
        comment: CommentId,
        reaction: Reaction,
        active: bool,
-
        signer: &Device<G>,
-
    ) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
-
        self.transaction("React comment", signer, |tx| {
+
    ) -> Result<EntryId, Error> {
+
        self.transaction("React comment", |tx| {
            tx.comment_react(revision, comment, reaction, active)
        })
    }

    /// Redact a comment on a patch revision.
-
    pub fn comment_redact<G>(
+
    pub fn comment_redact(
        &mut self,
        revision: RevisionId,
        comment: CommentId,
-
        signer: &Device<G>,
-
    ) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
-
        self.transaction("Redact comment", signer, |tx| {
-
            tx.comment_redact(revision, comment)
-
        })
+
    ) -> Result<EntryId, Error> {
+
        self.transaction("Redact comment", |tx| tx.comment_redact(revision, comment))
    }

    /// Comment on a line of code as part of a review.
-
    pub fn review_comment<G, S>(
+
    pub fn review_comment(
        &mut self,
        review: ReviewId,
-
        body: S,
+
        body: impl ToString,
        location: Option<CodeLocation>,
        reply_to: Option<CommentId>,
        embeds: impl IntoIterator<Item = Embed<Uri>>,
-
        signer: &Device<G>,
-
    ) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
        S: ToString,
-
    {
-
        self.transaction("Review comment", signer, |tx| {
+
    ) -> Result<EntryId, Error> {
+
        self.transaction("Review comment", |tx| {
            tx.review_comment(
                review,
                body,
@@ -2270,164 +2221,118 @@ where
    }

    /// Edit review comment.
-
    pub fn edit_review_comment<G, S>(
+
    pub fn edit_review_comment(
        &mut self,
        review: ReviewId,
        comment: EntryId,
-
        body: S,
+
        body: impl ToString,
        embeds: impl IntoIterator<Item = Embed<Uri>>,
-
        signer: &Device<G>,
-
    ) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
        S: ToString,
-
    {
-
        self.transaction("Edit review comment", signer, |tx| {
+
    ) -> Result<EntryId, Error> {
+
        self.transaction("Edit review comment", |tx| {
            tx.edit_review_comment(review, comment, body, embeds.into_iter().collect())
        })
    }

    /// React to a review comment.
-
    pub fn react_review_comment<G>(
+
    pub fn react_review_comment(
        &mut self,
        review: ReviewId,
        comment: EntryId,
        reaction: Reaction,
        active: bool,
-
        signer: &Device<G>,
-
    ) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
-
        self.transaction("React to review comment", signer, |tx| {
+
    ) -> Result<EntryId, Error> {
+
        self.transaction("React to review comment", |tx| {
            tx.react_review_comment(review, comment, reaction, active)
        })
    }

    /// React to a review comment.
-
    pub fn redact_review_comment<G>(
+
    pub fn redact_review_comment(
        &mut self,
        review: ReviewId,
        comment: EntryId,
-
        signer: &Device<G>,
-
    ) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
-
        self.transaction("Redact review comment", signer, |tx| {
+
    ) -> Result<EntryId, Error> {
+
        self.transaction("Redact review comment", |tx| {
            tx.redact_review_comment(review, comment)
        })
    }

    /// Review a patch revision.
-
    pub fn review<G>(
+
    pub fn review(
        &mut self,
        revision: RevisionId,
        verdict: Option<Verdict>,
        summary: Option<String>,
        labels: Vec<Label>,
-
        signer: &Device<G>,
-
    ) -> Result<ReviewId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
+
    ) -> Result<ReviewId, Error> {
        if verdict.is_none() && summary.is_none() {
            return Err(Error::EmptyReview);
        }
-
        self.transaction("Review", signer, |tx| {
-
            tx.review(revision, verdict, summary, labels)
-
        })
-
        .map(ReviewId)
+
        self.transaction("Review", |tx| tx.review(revision, verdict, summary, labels))
+
            .map(ReviewId)
    }

    /// Edit a review.
-
    pub fn review_edit<G>(
+
    pub fn review_edit(
        &mut self,
        review: ReviewId,
        verdict: Option<Verdict>,
        summary: String,
        labels: Vec<Label>,
        embeds: impl IntoIterator<Item = Embed<Uri>>,
-
        signer: &Device<G>,
-
    ) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
-
        self.transaction("Edit review", signer, |tx| {
+
    ) -> Result<EntryId, Error> {
+
        self.transaction("Edit review", |tx| {
            tx.review_edit(review, verdict, summary, labels, embeds)
        })
    }

    /// React to a review.
-
    pub fn review_react<G>(
+
    pub fn review_react(
        &mut self,
        review: ReviewId,
        reaction: Reaction,
        active: bool,
-
        signer: &Device<G>,
-
    ) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
-
        self.transaction("React to review", signer, |tx| {
+
    ) -> Result<EntryId, Error> {
+
        self.transaction("React to review", |tx| {
            tx.review_react(review, reaction, active)
        })
    }

    /// Redact a patch review.
-
    pub fn redact_review<G>(
-
        &mut self,
-
        review: ReviewId,
-
        signer: &Device<G>,
-
    ) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
-
        self.transaction("Redact review", signer, |tx| tx.redact_review(review))
+
    pub fn redact_review(&mut self, review: ReviewId) -> Result<EntryId, Error> {
+
        self.transaction("Redact review", |tx| tx.redact_review(review))
    }

    /// Resolve a patch review comment.
-
    pub fn resolve_review_comment<G>(
+
    pub fn resolve_review_comment(
        &mut self,
        review: ReviewId,
        comment: CommentId,
-
        signer: &Device<G>,
-
    ) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
-
        self.transaction("Resolve review comment", signer, |tx| {
+
    ) -> Result<EntryId, Error> {
+
        self.transaction("Resolve review comment", |tx| {
            tx.review_comment_resolve(review, comment)
        })
    }

    /// Unresolve a patch review comment.
-
    pub fn unresolve_review_comment<G>(
+
    pub fn unresolve_review_comment(
        &mut self,
        review: ReviewId,
        comment: CommentId,
-
        signer: &Device<G>,
-
    ) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
-
        self.transaction("Unresolve review comment", signer, |tx| {
+
    ) -> Result<EntryId, Error> {
+
        self.transaction("Unresolve review comment", |tx| {
            tx.review_comment_unresolve(review, comment)
        })
    }

    /// Merge a patch revision.
-
    pub fn merge<G>(
+
    pub fn merge(
        &mut self,
        revision: RevisionId,
        commit: git::Oid,
-
        signer: &Device<G>,
-
    ) -> Result<Merged<'_, R>, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
+
    ) -> Result<Merged<'_, Repo>, Error> {
        // TODO: Don't allow merging the same revision twice?
-
        let entry = self.transaction("Merge revision", signer, |tx| tx.merge(revision, commit))?;
+
        let entry = self.transaction("Merge revision", |tx| tx.merge(revision, commit))?;

        Ok(Merged {
            entry,
@@ -2437,108 +2342,73 @@ where
    }

    /// Update a patch with a new revision.
-
    pub fn update<G>(
+
    pub fn update(
        &mut self,
        description: impl ToString,
        base: impl Into<git::Oid>,
        oid: impl Into<git::Oid>,
-
        signer: &Device<G>,
-
    ) -> Result<RevisionId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
-
        self.transaction("Add revision", signer, |tx| {
-
            tx.revision(description, base, oid)
-
        })
-
        .map(RevisionId)
+
    ) -> Result<RevisionId, Error> {
+
        self.transaction("Add revision", |tx| tx.revision(description, base, oid))
+
            .map(RevisionId)
    }

    /// Lifecycle a patch.
-
    pub fn lifecycle<G>(&mut self, state: Lifecycle, signer: &Device<G>) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
-
        self.transaction("Lifecycle", signer, |tx| tx.lifecycle(state))
+
    pub fn lifecycle(&mut self, state: Lifecycle) -> Result<EntryId, Error> {
+
        self.transaction("Lifecycle", |tx| tx.lifecycle(state))
    }

    /// Assign a patch.
-
    pub fn assign<G>(
-
        &mut self,
-
        assignees: BTreeSet<Did>,
-
        signer: &Device<G>,
-
    ) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
-
        self.transaction("Assign", signer, |tx| tx.assign(assignees))
+
    pub fn assign(&mut self, assignees: BTreeSet<Did>) -> Result<EntryId, Error> {
+
        self.transaction("Assign", |tx| tx.assign(assignees))
    }

    /// Archive a patch.
-
    pub fn archive<G>(&mut self, signer: &Device<G>) -> Result<bool, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
-
        self.lifecycle(Lifecycle::Archived, signer)?;
+
    pub fn archive(&mut self) -> Result<bool, Error> {
+
        self.lifecycle(Lifecycle::Archived)?;

        Ok(true)
    }

    /// Mark an archived patch as ready to be reviewed again.
    /// Returns `false` if the patch was not archived.
-
    pub fn unarchive<G>(&mut self, signer: &Device<G>) -> Result<bool, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
+
    pub fn unarchive(&mut self) -> Result<bool, Error> {
        if !self.is_archived() {
            return Ok(false);
        }
-
        self.lifecycle(Lifecycle::Open, signer)?;
+
        self.lifecycle(Lifecycle::Open)?;

        Ok(true)
    }

    /// Mark a patch as ready to be reviewed.
    /// Returns `false` if the patch was not a draft.
-
    pub fn ready<G>(&mut self, signer: &Device<G>) -> Result<bool, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
+
    pub fn ready(&mut self) -> Result<bool, Error> {
        if !self.is_draft() {
            return Ok(false);
        }
-
        self.lifecycle(Lifecycle::Open, signer)?;
+
        self.lifecycle(Lifecycle::Open)?;

        Ok(true)
    }

    /// Mark an open patch as a draft.
    /// Returns `false` if the patch was not open and free of merges.
-
    pub fn unready<G>(&mut self, signer: &Device<G>) -> Result<bool, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
+
    pub fn unready(&mut self) -> Result<bool, Error> {
        if !matches!(self.state(), State::Open { conflicts } if conflicts.is_empty()) {
            return Ok(false);
        }
-
        self.lifecycle(Lifecycle::Draft, signer)?;
+
        self.lifecycle(Lifecycle::Draft)?;

        Ok(true)
    }

    /// Label a patch.
-
    pub fn label<G>(
-
        &mut self,
-
        labels: impl IntoIterator<Item = Label>,
-
        signer: &Device<G>,
-
    ) -> Result<EntryId, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
-
        self.transaction("Label", signer, |tx| tx.label(labels))
+
    pub fn label(&mut self, labels: impl IntoIterator<Item = Label>) -> Result<EntryId, Error> {
+
        self.transaction("Label", |tx| tx.label(labels))
    }
}

-
impl<R, C> Deref for PatchMut<'_, '_, R, C> {
+
impl<Repo, Signer, Cache> Deref for PatchMut<'_, '_, '_, Repo, Signer, Cache> {
    type Target = Patch;

    fn deref(&self) -> &Self::Target {
@@ -2574,39 +2444,46 @@ pub struct ByRevision {
    pub revision: Revision,
}

-
pub struct Patches<'a, R> {
-
    raw: store::Store<'a, Patch, R>,
+
pub struct Patches<'a, Repo, Access> {
+
    raw: store::Store<'a, Patch, Repo, Access>,
}

-
impl<'a, R> Deref for Patches<'a, R> {
-
    type Target = store::Store<'a, Patch, R>;
+
impl<'a, Repo, Access> Deref for Patches<'a, Repo, Access> {
+
    type Target = store::Store<'a, Patch, Repo, Access>;

    fn deref(&self) -> &Self::Target {
        &self.raw
    }
}

-
impl<R> HasRepoId for Patches<'_, R>
+
impl<Repo, Access> HasRepoId for Patches<'_, Repo, Access>
where
-
    R: ReadRepository,
+
    Repo: HasRepoId,
{
    fn rid(&self) -> RepoId {
-
        self.as_ref().id()
+
        self.raw.rid()
    }
}

-
impl<'a, R> Patches<'a, R>
+
impl<'a, Repo, Access> Patches<'a, Repo, Access>
where
-
    R: ReadRepository + cob::Store<Namespace = NodeId>,
+
    Repo: ReadRepository + cob::Store<Namespace = NodeId>,
+
    Access: store::access::Access,
{
    /// Open a patches store.
-
    pub fn open(repository: &'a R) -> Result<Self, RepositoryError> {
+
    pub fn open(repository: &'a Repo, access: Access) -> Result<Self, RepositoryError> {
        let identity = repository.identity_head()?;
-
        let raw = store::Store::open(repository)?.identity(identity);
+
        let raw = store::Store::open(repository, access)?.identity(identity);

        Ok(Self { raw })
    }
+
}

+
impl<'a, Repo, Access> Patches<'a, Repo, Access>
+
where
+
    Repo: ReadRepository + cob::Store<Namespace = NodeId>,
+
    Access: store::access::Access,
+
{
    /// Patches count by state.
    pub fn counts(&self) -> Result<PatchCounts, store::Error> {
        let all = self.all()?;
@@ -2678,12 +2555,14 @@ where
    }
}

-
impl<'a, R> Patches<'a, R>
+
impl<'a, 'b, Repo, Signer> Patches<'a, Repo, WriteAs<'b, Signer>>
where
-
    R: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
+
    Repo: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
+
    Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
    Signer: crypto::signature::Signer<crypto::Signature>,
{
    /// Open a new patch.
-
    pub fn create<'g, C, G>(
+
    pub fn create<'g, Cache>(
        &'g mut self,
        title: cob::Title,
        description: impl ToString,
@@ -2691,12 +2570,13 @@ where
        base: impl Into<git::Oid>,
        oid: impl Into<git::Oid>,
        labels: &[Label],
-
        cache: &'g mut C,
-
        signer: &Device<G>,
-
    ) -> Result<PatchMut<'a, 'g, R, C>, Error>
+
        cache: &'g mut Cache,
+
    ) -> Result<PatchMut<'a, 'b, 'g, Repo, Signer, Cache>, Error>
    where
-
        C: cob::cache::Update<Patch>,
-
        G: crypto::signature::Signer<crypto::Signature>,
+
        Cache: cob::cache::Update<Patch>,
+
        Signer: crypto::signature::Signer<crypto::Signature>,
+
        Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
+
        Signer: crypto::signature::Verifier<crypto::Signature>,
    {
        self._create(
            title,
@@ -2707,12 +2587,11 @@ where
            labels,
            Lifecycle::default(),
            cache,
-
            signer,
        )
    }

    /// Draft a patch. This patch will be created in a [`State::Draft`] state.
-
    pub fn draft<'g, C, G>(
+
    pub fn draft<'g, Cache>(
        &'g mut self,
        title: cob::Title,
        description: impl ToString,
@@ -2720,12 +2599,12 @@ where
        base: impl Into<git::Oid>,
        oid: impl Into<git::Oid>,
        labels: &[Label],
-
        cache: &'g mut C,
-
        signer: &Device<G>,
-
    ) -> Result<PatchMut<'a, 'g, R, C>, Error>
+
        cache: &'g mut Cache,
+
    ) -> Result<PatchMut<'a, 'b, 'g, Repo, Signer, Cache>, Error>
    where
-
        C: cob::cache::Update<Patch>,
-
        G: crypto::signature::Signer<crypto::Signature>,
+
        Cache: cob::cache::Update<Patch>,
+
        Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
+
        Signer: crypto::signature::Verifier<crypto::Signature>,
    {
        self._create(
            title,
@@ -2736,16 +2615,15 @@ where
            labels,
            Lifecycle::Draft,
            cache,
-
            signer,
        )
    }

    /// Get a patch mutably.
-
    pub fn get_mut<'g, C>(
+
    pub fn get_mut<'g, Cache>(
        &'g mut self,
        id: &ObjectId,
-
        cache: &'g mut C,
-
    ) -> Result<PatchMut<'a, 'g, R, C>, store::Error> {
+
        cache: &'g mut Cache,
+
    ) -> Result<PatchMut<'a, 'b, 'g, Repo, Signer, Cache>, store::Error> {
        let patch = self
            .raw
            .get(id)?
@@ -2760,7 +2638,7 @@ where
    }

    /// Create a patch. This is an internal function used by `create` and `draft`.
-
    fn _create<'g, C, G>(
+
    fn _create<'g, Cache>(
        &'g mut self,
        title: cob::Title,
        description: impl ToString,
@@ -2769,14 +2647,16 @@ where
        oid: impl Into<git::Oid>,
        labels: &[Label],
        state: Lifecycle,
-
        cache: &'g mut C,
-
        signer: &Device<G>,
-
    ) -> Result<PatchMut<'a, 'g, R, C>, Error>
+
        cache: &'g mut Cache,
+
    ) -> Result<PatchMut<'a, 'b, 'g, Repo, Signer, Cache>, Error>
    where
-
        C: cob::cache::Update<Patch>,
-
        G: crypto::signature::Signer<crypto::Signature>,
+
        Cache: cob::cache::Update<Patch>,
+
        Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
        Signer: crypto::signature::Signer<crypto::Signature>,
+
        Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
+
        Signer: crypto::signature::Verifier<crypto::Signature>,
    {
-
        let (id, patch) = Transaction::initial("Create patch", &mut self.raw, signer, |tx, _| {
+
        let (id, patch) = Transaction::initial("Create patch", &mut self.raw, |tx, _| {
            tx.revision(description, base, oid)?;
            tx.edit(title, target)?;

@@ -3062,7 +2942,7 @@ mod test {
        let alice = test::setup::NodeWithRepo::default();
        let checkout = alice.repo.checkout();
        let branch = checkout.branch_with([("README", b"Hello World!")]);
-
        let mut patches = Cache::no_cache(&*alice.repo).unwrap();
+
        let mut patches = Cache::no_cache(&*alice.repo, &alice.signer).unwrap();
        let author: Did = alice.signer.public_key().into();
        let target = MergeTarget::Delegates;
        let patch = patches
@@ -3073,7 +2953,6 @@ mod test {
                branch.base,
                branch.oid,
                &[],
-
                &alice.signer,
            )
            .unwrap();

@@ -3104,7 +2983,7 @@ mod test {
        let alice = test::setup::NodeWithRepo::default();
        let checkout = alice.repo.checkout();
        let branch = checkout.branch_with([("README", b"Hello World!")]);
-
        let mut patches = Cache::no_cache(&*alice.repo).unwrap();
+
        let mut patches = Cache::no_cache(&*alice.repo, &alice.signer).unwrap();
        let patch = patches
            .create(
                cob::Title::new("My first patch").unwrap(),
@@ -3113,7 +2992,6 @@ mod test {
                branch.base,
                branch.oid,
                &[],
-
                &alice.signer,
            )
            .unwrap();

@@ -3122,7 +3000,7 @@ mod test {
        let (revision_id, _) = patch.revisions().last().unwrap();
        assert!(
            patch
-
                .comment(revision_id, "patch comment", None, None, [], &alice.signer)
+
                .comment(revision_id, "patch comment", None, None, [],)
                .is_ok(),
            "can comment on patch"
        );
@@ -3137,7 +3015,7 @@ mod test {
        let alice = test::setup::NodeWithRepo::default();
        let checkout = alice.repo.checkout();
        let branch = checkout.branch_with([("README", b"Hello World!")]);
-
        let mut patches = Cache::no_cache(&*alice.repo).unwrap();
+
        let mut patches = Cache::no_cache(&*alice.repo, &alice.signer).unwrap();
        let mut patch = patches
            .create(
                cob::Title::new("My first patch").unwrap(),
@@ -3146,13 +3024,12 @@ mod test {
                branch.base,
                branch.oid,
                &[],
-
                &alice.signer,
            )
            .unwrap();

        let id = patch.id;
        let (rid, _) = patch.revisions().next().unwrap();
-
        let _merge = patch.merge(rid, branch.base, &alice.signer).unwrap();
+
        let _merge = patch.merge(rid, branch.base).unwrap();
        let patch = patches.get(&id).unwrap().unwrap();

        let merges = patch.merges.iter().collect::<Vec<_>>();
@@ -3168,7 +3045,7 @@ mod test {
        let alice = test::setup::NodeWithRepo::default();
        let checkout = alice.repo.checkout();
        let branch = checkout.branch_with([("README", b"Hello World!")]);
-
        let mut patches = Cache::no_cache(&*alice.repo).unwrap();
+
        let mut patches = Cache::no_cache(&*alice.repo, &alice.signer).unwrap();
        let mut patch = patches
            .create(
                cob::Title::new("My first patch").unwrap(),
@@ -3177,7 +3054,6 @@ mod test {
                branch.base,
                branch.oid,
                &[],
-
                &alice.signer,
            )
            .unwrap();

@@ -3188,7 +3064,6 @@ mod test {
                Some(Verdict::Accept),
                Some("LGTM".to_owned()),
                vec![],
-
                &alice.signer,
            )
            .unwrap();

@@ -3201,17 +3076,17 @@ mod test {
        assert_eq!(review.verdict(), Some(Verdict::Accept));
        assert_eq!(review.summary(), "LGTM");

-
        patch.redact_review(review_id, &alice.signer).unwrap();
+
        patch.redact_review(review_id).unwrap();
        patch.reload().unwrap();

        let (_, revision) = patch.latest();
        assert_eq!(revision.reviews().count(), 0);

        // This is fine, redacting an already-redacted review is a no-op.
-
        patch.redact_review(review_id, &alice.signer).unwrap();
+
        patch.redact_review(review_id).unwrap();
        // If the review never existed, it's an error.
        patch
-
            .redact_review(ReviewId(arbitrary::entry_id()), &alice.signer)
+
            .redact_review(ReviewId(arbitrary::entry_id()))
            .unwrap_err();
    }

@@ -3220,7 +3095,7 @@ mod test {
        let alice = test::setup::NodeWithRepo::default();
        let checkout = alice.repo.checkout();
        let branch = checkout.branch_with([("README", b"Hello World!")]);
-
        let mut patches = Cache::no_cache(&*alice.repo).unwrap();
+
        let mut patches = Cache::no_cache(&*alice.repo, &alice.signer).unwrap();
        let mut patch = patches
            .create(
                cob::Title::new("My first patch").unwrap(),
@@ -3229,21 +3104,20 @@ mod test {
                branch.base,
                branch.oid,
                &[],
-
                &alice.signer,
            )
            .unwrap();

        let update = checkout.branch_with([("README", b"Hello Radicle!")]);
        let updated = patch
-
            .update("I've made changes.", branch.base, update.oid, &alice.signer)
+
            .update("I've made changes.", branch.base, update.oid)
            .unwrap();

        // It's fine to redact a review from a redacted revision.
        let review = patch
-
            .review(updated, Some(Verdict::Accept), None, vec![], &alice.signer)
+
            .review(updated, Some(Verdict::Accept), None, vec![])
            .unwrap();
-
        patch.redact(updated, &alice.signer).unwrap();
-
        patch.redact_review(review, &alice.signer).unwrap();
+
        patch.redact(updated).unwrap();
+
        patch.redact_review(review).unwrap();
    }

    #[test]
@@ -3408,7 +3282,7 @@ mod test {
        let alice = test::setup::NodeWithRepo::default();
        let checkout = alice.repo.checkout();
        let branch = checkout.branch_with([("README", b"Hello World!")]);
-
        let mut patches = Cache::no_cache(&*alice.repo).unwrap();
+
        let mut patches = Cache::no_cache(&*alice.repo, &alice.signer).unwrap();
        let mut patch = patches
            .create(
                cob::Title::new("My first patch").unwrap(),
@@ -3417,19 +3291,12 @@ mod test {
                branch.base,
                branch.oid,
                &[],
-
                &alice.signer,
            )
            .unwrap();

        let (rid, _) = patch.latest();
        let review = patch
-
            .review(
-
                rid,
-
                Some(Verdict::Accept),
-
                Some("LGTM".to_owned()),
-
                vec![],
-
                &alice.signer,
-
            )
+
            .review(rid, Some(Verdict::Accept), Some("LGTM".to_owned()), vec![])
            .unwrap();
        patch
            .review_edit(
@@ -3438,7 +3305,6 @@ mod test {
                "Whoops!".to_owned(),
                vec![],
                vec![],
-
                &alice.signer,
            )
            .unwrap(); // Overwrite the comment.

@@ -3453,7 +3319,7 @@ mod test {
        let alice = test::setup::NodeWithRepo::default();
        let checkout = alice.repo.checkout();
        let branch = checkout.branch_with([("README", b"Hello World!")]);
-
        let mut patches = Cache::no_cache(&*alice.repo).unwrap();
+
        let mut patches = Cache::no_cache(&*alice.repo, &alice.signer).unwrap();
        let mut patch = patches
            .create(
                cob::Title::new("My first patch").unwrap(),
@@ -3462,16 +3328,15 @@ mod test {
                branch.base,
                branch.oid,
                &[],
-
                &alice.signer,
            )
            .unwrap();

        let (rid, _) = patch.latest();
        patch
-
            .review(rid, Some(Verdict::Accept), None, vec![], &alice.signer)
+
            .review(rid, Some(Verdict::Accept), None, vec![])
            .unwrap();
        patch
-
            .review(rid, Some(Verdict::Reject), None, vec![], &alice.signer)
+
            .review(rid, Some(Verdict::Reject), None, vec![])
            .unwrap(); // This review is ignored, since there is already a review by this author.

        let (_, revision) = patch.latest();
@@ -3484,7 +3349,7 @@ mod test {
        let alice = test::setup::NodeWithRepo::default();
        let checkout = alice.repo.checkout();
        let branch = checkout.branch_with([("README", b"Hello World!")]);
-
        let mut patches = Cache::no_cache(&*alice.repo).unwrap();
+
        let mut patches = Cache::no_cache(&*alice.repo, &alice.signer).unwrap();
        let mut patch = patches
            .create(
                cob::Title::new("My first patch").unwrap(),
@@ -3493,16 +3358,15 @@ mod test {
                branch.base,
                branch.oid,
                &[],
-
                &alice.signer,
            )
            .unwrap();

        let (rid, _) = patch.latest();
        let review = patch
-
            .review(rid, Some(Verdict::Accept), None, vec![], &alice.signer)
+
            .review(rid, Some(Verdict::Accept), None, vec![])
            .unwrap();
        patch
-
            .review_comment(review, "First comment!", None, None, [], &alice.signer)
+
            .review_comment(review, "First comment!", None, None, [])
            .unwrap();

        let _review = patch
@@ -3512,11 +3376,10 @@ mod test {
                "".to_string(),
                vec![],
                vec![],
-
                &alice.signer,
            )
            .unwrap();
        patch
-
            .review_comment(review, "Second comment!", None, None, [], &alice.signer)
+
            .review_comment(review, "Second comment!", None, None, [])
            .unwrap();

        let (_, revision) = patch.latest();
@@ -3535,7 +3398,7 @@ mod test {
        let alice = test::setup::NodeWithRepo::default();
        let checkout = alice.repo.checkout();
        let branch = checkout.branch_with([("README", b"Hello World!")]);
-
        let mut patches = Cache::no_cache(&*alice.repo).unwrap();
+
        let mut patches = Cache::no_cache(&*alice.repo, &alice.signer).unwrap();
        let mut patch = patches
            .create(
                cob::Title::new("My first patch").unwrap(),
@@ -3544,7 +3407,6 @@ mod test {
                branch.base,
                branch.oid,
                &[],
-
                &alice.signer,
            )
            .unwrap();

@@ -3556,7 +3418,7 @@ mod test {
            new: Some(CodeRange::Lines { range: 5..8 }),
        };
        let review = patch
-
            .review(rid, Some(Verdict::Accept), None, vec![], &alice.signer)
+
            .review(rid, Some(Verdict::Accept), None, vec![])
            .unwrap();
        patch
            .review_comment(
@@ -3565,7 +3427,6 @@ mod test {
                Some(location.clone()),
                None,
                [],
-
                &alice.signer,
            )
            .unwrap();

@@ -3582,7 +3443,7 @@ mod test {
        let alice = test::setup::NodeWithRepo::default();
        let checkout = alice.repo.checkout();
        let branch = checkout.branch_with([("README", b"Hello World!")]);
-
        let mut patches = Cache::no_cache(&*alice.repo).unwrap();
+
        let mut patches = Cache::no_cache(&*alice.repo, &alice.signer).unwrap();
        let mut patch = patches
            .create(
                cob::Title::new("My first patch").unwrap(),
@@ -3591,19 +3452,12 @@ mod test {
                branch.base,
                branch.oid,
                &[],
-
                &alice.signer,
            )
            .unwrap();

        let (rid, _) = patch.latest();
        let review = patch
-
            .review(
-
                rid,
-
                Some(Verdict::Accept),
-
                Some("Nah".to_owned()),
-
                vec![],
-
                &alice.signer,
-
            )
+
            .review(rid, Some(Verdict::Accept), Some("Nah".to_owned()), vec![])
            .unwrap();
        patch
            .review_edit(
@@ -3612,7 +3466,6 @@ mod test {
                "".to_string(),
                vec![],
                vec![],
-
                &alice.signer,
            )
            .unwrap();

@@ -3633,7 +3486,8 @@ mod test {
        let mut patches = {
            let path = alice.tmp.path().join("cobs.db");
            let mut db = cob::cache::Store::open(path).unwrap();
-
            let store = cob::patch::Patches::open(&*alice.repo).unwrap();
+
            let store =
+
                cob::patch::Patches::open(&*alice.repo, WriteAs::new(&alice.signer)).unwrap();

            db.migrate(migrate::ignore).unwrap();
            cob::patch::Cache::open(store, db)
@@ -3646,7 +3500,6 @@ mod test {
                branch.base,
                branch.oid,
                &[],
-
                &alice.signer,
            )
            .unwrap();

@@ -3655,7 +3508,7 @@ mod test {

        let update = checkout.branch_with([("README", b"Hello Radicle!")]);
        let _ = patch
-
            .update("I've made changes.", branch.base, update.oid, &alice.signer)
+
            .update("I've made changes.", branch.base, update.oid)
            .unwrap();

        let id = patch.id;
@@ -3686,7 +3539,7 @@ mod test {
        let branch = repo
            .checkout()
            .branch_with([("README.md", b"Hello, World!")]);
-
        let mut patches = Cache::no_cache(&*repo).unwrap();
+
        let mut patches = Cache::no_cache(&*repo, &alice.signer).unwrap();
        let mut patch = patches
            .create(
                cob::Title::new("My first patch").unwrap(),
@@ -3695,7 +3548,6 @@ mod test {
                branch.base,
                branch.oid,
                &[],
-
                &alice.signer,
            )
            .unwrap();
        let patch_id = patch.id;
@@ -3704,17 +3556,17 @@ mod test {
            .checkout()
            .branch_with([("README.md", b"Hello, Radicle!")]);
        let revision_id = patch
-
            .update("I've made changes.", branch.base, update.oid, &alice.signer)
+
            .update("I've made changes.", branch.base, update.oid)
            .unwrap();
        assert_eq!(patch.revisions().count(), 2);

-
        patch.redact(revision_id, &alice.signer).unwrap();
+
        patch.redact(revision_id).unwrap();
        assert_eq!(patch.latest().0, RevisionId(*patch_id));
        assert_eq!(patch.revisions().count(), 1);

        // The patch's root must always exist.
        assert_eq!(patch.latest(), patch.root());
-
        assert!(patch.redact(patch.latest().0, &alice.signer).is_err());
+
        assert!(patch.redact(patch.latest().0).is_err());
    }

    #[test]
modified crates/radicle/src/cob/patch/cache.rs
@@ -8,9 +8,9 @@ use crate::cob;
use crate::cob::cache::{self, StoreReader};
use crate::cob::cache::{Remove, StoreWriter, Update};
use crate::cob::store;
+
use crate::cob::store::access::{ReadOnly, WriteAs};
use crate::cob::{Label, ObjectId, TypeName};
use crate::git;
-
use crate::node::device::Device;
use crate::prelude::RepoId;
use crate::storage::{HasRepoId, ReadRepository, RepositoryError, SignRepository, WriteRepository};

@@ -87,28 +87,36 @@ impl<T> PatchesMut for T where T: Patches + Update<Patch> + Remove<Patch> {}
/// The `store` is used for the main storage when performing a
/// write-through. It is also used for identifying which `RepoId` is
/// being used for the `cache`.
-
pub struct Cache<R, C> {
-
    pub(super) store: R,
+
pub struct Cache<'a, Repo, Access, C> {
+
    pub(super) store: super::Patches<'a, Repo, Access>,
    pub(super) cache: C,
}

-
impl<R, C> Cache<R, C> {
-
    pub fn new(store: R, cache: C) -> Self {
+
impl<'a, Repo, Access, C> Cache<'a, Repo, Access, C> {
+
    pub fn new(store: super::Patches<'a, Repo, Access>, cache: C) -> Self {
        Self { store, cache }
    }
+
}

-
    pub fn rid(&self) -> RepoId
-
    where
-
        R: HasRepoId,
-
    {
+
impl<'a, Repo, Access, C> HasRepoId for Cache<'a, Repo, Access, C>
+
where
+
    Repo: HasRepoId,
+
{
+
    fn rid(&self) -> RepoId {
        self.store.rid()
    }
}

-
impl<'a, R, C> Cache<super::Patches<'a, R>, C> {
+
impl<'a, 'b, Repo, Signer, C> Cache<'a, Repo, WriteAs<'b, Signer>, C>
+
where
+
    Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
    Signer: crypto::signature::Signer<crypto::Signature>,
+
    Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
+
    Signer: crypto::signature::Verifier<crypto::Signature>,
+
{
    /// Create a new [`Patch`] using the [`super::Patches`] as the
    /// main storage, and writing the update to the `cache`.
-
    pub fn create<'g, G>(
+
    pub fn create<'g>(
        &'g mut self,
        title: cob::Title,
        description: impl ToString,
@@ -116,11 +124,9 @@ impl<'a, R, C> Cache<super::Patches<'a, R>, C> {
        base: impl Into<git::Oid>,
        oid: impl Into<git::Oid>,
        labels: &[Label],
-
        signer: &Device<G>,
-
    ) -> Result<PatchMut<'a, 'g, R, C>, super::Error>
+
    ) -> Result<PatchMut<'a, 'b, 'g, Repo, Signer, C>, super::Error>
    where
-
        R: WriteRepository + cob::Store<Namespace = NodeId>,
-
        G: crypto::signature::Signer<crypto::Signature>,
+
        Repo: WriteRepository + cob::Store<Namespace = NodeId>,
        C: Update<Patch>,
    {
        self.store.create(
@@ -131,14 +137,13 @@ impl<'a, R, C> Cache<super::Patches<'a, R>, C> {
            oid,
            labels,
            &mut self.cache,
-
            signer,
        )
    }

    /// Create a new [`Patch`], in a draft state, using the
    /// [`super::Patches`] as the main storage, and writing the update
    /// to the `cache`.
-
    pub fn draft<'g, G>(
+
    pub fn draft<'g>(
        &'g mut self,
        title: cob::Title,
        description: impl ToString,
@@ -146,11 +151,9 @@ impl<'a, R, C> Cache<super::Patches<'a, R>, C> {
        base: impl Into<git::Oid>,
        oid: impl Into<git::Oid>,
        labels: &[Label],
-
        signer: &Device<G>,
-
    ) -> Result<PatchMut<'a, 'g, R, C>, super::Error>
+
    ) -> Result<PatchMut<'a, 'b, 'g, Repo, Signer, C>, super::Error>
    where
-
        R: WriteRepository + cob::Store<Namespace = NodeId>,
-
        G: crypto::signature::Signer<crypto::Signature>,
+
        Repo: WriteRepository + cob::Store<Namespace = NodeId>,
        C: Update<Patch>,
    {
        self.store.draft(
@@ -161,19 +164,17 @@ impl<'a, R, C> Cache<super::Patches<'a, R>, C> {
            oid,
            labels,
            &mut self.cache,
-
            signer,
        )
    }

    /// Remove the given `id` from the [`super::Patches`] storage, and
    /// removing the entry from the `cache`.
-
    pub fn remove<G>(&mut self, id: &PatchId, signer: &Device<G>) -> Result<(), super::Error>
+
    pub fn remove(&mut self, id: &PatchId) -> Result<(), super::Error>
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
        R: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
+
        Repo: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
        C: Remove<Patch>,
    {
-
        self.store.remove(id, signer)?;
+
        self.store.raw.remove(id)?;
        self.cache
            .remove(id)
            .map_err(|e| super::Error::CacheRemove {
@@ -182,12 +183,17 @@ impl<'a, R, C> Cache<super::Patches<'a, R>, C> {
            })?;
        Ok(())
    }
+
}

+
impl<'a, Repo, Access, C> Cache<'a, Repo, Access, C>
+
where
+
    Access: cob::store::access::Access,
+
{
    /// Read the given `id` from the [`super::Patches`] store and
    /// writing it to the `cache`.
    pub fn write(&mut self, id: &PatchId) -> Result<(), super::Error>
    where
-
        R: ReadRepository + cob::Store,
+
        Repo: ReadRepository + cob::Store<Namespace = NodeId>,
        C: Update<Patch>,
    {
        let issue = self
@@ -213,7 +219,7 @@ impl<'a, R, C> Cache<super::Patches<'a, R>, C> {
        callback: impl Fn(&Result<(PatchId, Patch), store::Error>, &cache::Progress) -> ControlFlow<()>,
    ) -> Result<(), super::Error>
    where
-
        R: ReadRepository + cob::Store,
+
        Repo: ReadRepository + cob::Store<Namespace = NodeId>,
        C: Update<Patch> + Remove<Patch>,
    {
        // Start by clearing the cache. This will get rid of patches that are cached but
@@ -240,28 +246,28 @@ impl<'a, R, C> Cache<super::Patches<'a, R>, C> {
    }
}

-
impl<R> Cache<R, StoreReader> {
-
    pub fn reader(store: R, cache: StoreReader) -> Self {
+
impl<'a, Repo> Cache<'a, Repo, ReadOnly, StoreReader> {
+
    pub fn reader(store: super::Patches<'a, Repo, ReadOnly>, cache: StoreReader) -> Self {
        Self { store, cache }
    }
}

-
impl<R> Cache<R, StoreWriter> {
-
    pub fn open(store: R, cache: StoreWriter) -> Self {
+
impl<'a, Repo, Access> Cache<'a, Repo, Access, StoreWriter> {
+
    pub fn open(store: super::Patches<'a, Repo, Access>, cache: StoreWriter) -> Self {
        Self { store, cache }
    }
}

-
impl<'a, R> Cache<super::Patches<'a, R>, StoreWriter>
+
impl<'a, 'b, Repo, Signer> Cache<'a, Repo, WriteAs<'b, Signer>, StoreWriter>
where
-
    R: ReadRepository + cob::Store,
+
    Repo: ReadRepository + cob::Store<Namespace = NodeId>,
{
    /// Get the [`PatchMut`], identified by `id`, using the
    /// `StoreWriter` for retrieving the `Patch`.
    pub fn get_mut<'g>(
        &'g mut self,
        id: &ObjectId,
-
    ) -> Result<PatchMut<'a, 'g, R, StoreWriter>, Error> {
+
    ) -> Result<PatchMut<'a, 'b, 'g, Repo, Signer, StoreWriter>, Error> {
        let patch = Patches::get(self, id)?
            .ok_or_else(move || Error::NotFound(super::TYPENAME.clone(), *id))?;

@@ -274,14 +280,14 @@ where
    }
}

-
impl<'a, R> Cache<super::Patches<'a, R>, cache::NoCache>
+
impl<'a, 'b, Repo, Signer> Cache<'a, Repo, WriteAs<'b, Signer>, cache::NoCache>
where
-
    R: ReadRepository + cob::Store<Namespace = NodeId>,
+
    Repo: ReadRepository + cob::Store<Namespace = NodeId>,
{
    /// Get a `Cache` that does no write-through modifications and
    /// uses the [`super::Patches`] store for all reads and writes.
-
    pub fn no_cache(repository: &'a R) -> Result<Self, RepositoryError> {
-
        let store = super::Patches::open(repository)?;
+
    pub fn no_cache(repository: &'a Repo, signer: &'b Signer) -> Result<Self, RepositoryError> {
+
        let store = super::Patches::open(repository, WriteAs::new(signer))?;
        Ok(Self {
            store,
            cache: cache::NoCache,
@@ -292,7 +298,7 @@ where
    pub fn get_mut<'g>(
        &'g mut self,
        id: &ObjectId,
-
    ) -> Result<PatchMut<'a, 'g, R, cache::NoCache>, super::Error> {
+
    ) -> Result<PatchMut<'a, 'b, 'g, Repo, Signer, cache::NoCache>, super::Error> {
        let patch = self
            .store
            .get(id)?
@@ -307,7 +313,7 @@ where
    }
}

-
impl<R, C> cache::Update<Patch> for Cache<R, C>
+
impl<'a, Repo, Access, C> cache::Update<Patch> for Cache<'a, Repo, Access, C>
where
    C: cache::Update<Patch>,
{
@@ -324,7 +330,7 @@ where
    }
}

-
impl<R, C> cache::Remove<Patch> for Cache<R, C>
+
impl<'a, Repo, Access, C> cache::Remove<Patch> for Cache<'a, Repo, Access, C>
where
    C: cache::Remove<Patch>,
{
@@ -443,9 +449,9 @@ impl Iterator for PatchesIter<'_> {
    }
}

-
impl<R> Patches for Cache<R, StoreReader>
+
impl<'a, Repo, Access> Patches for Cache<'a, Repo, Access, StoreReader>
where
-
    R: HasRepoId,
+
    Repo: HasRepoId,
{
    type Error = Error;
    type Iter<'b>
@@ -486,9 +492,10 @@ impl Iterator for NoCacheIter<'_> {
    }
}

-
impl<R> Patches for Cache<super::Patches<'_, R>, cache::NoCache>
+
impl<'a, Repo, Access> Patches for Cache<'a, Repo, Access, cache::NoCache>
where
-
    R: ReadRepository + cob::Store<Namespace = NodeId>,
+
    Repo: ReadRepository + cob::Store<Namespace = NodeId>,
+
    Access: store::access::Access,
{
    type Error = super::Error;
    type Iter<'b>
@@ -535,9 +542,10 @@ where
    }
}

-
impl<R> Patches for Cache<R, StoreWriter>
+
impl<'a, Repo, Access> Patches for Cache<'a, Repo, Access, StoreWriter>
where
-
    R: HasRepoId,
+
    Repo: HasRepoId + cob::Store<Namespace = NodeId>,
+
    Access: store::access::Access,
{
    type Error = Error;
    type Iter<'b>
@@ -711,6 +719,7 @@ mod tests {
    use radicle_cob::ObjectId;

    use crate::cob::cache::{Store, Update, Write};
+
    use crate::cob::store::access::ReadOnly;
    use crate::cob::thread::{Comment, Thread};
    use crate::cob::{Author, Title, migrate};
    use crate::patch::{
@@ -718,12 +727,14 @@ mod tests {
    };
    use crate::prelude::Did;
    use crate::profile::env;
+
    use crate::storage::HasRepoId as _;
    use crate::test::arbitrary;
    use crate::test::storage::MockRepository;

    use super::{Cache, Patches};

-
    fn memory(store: MockRepository) -> Cache<MockRepository, Store<Write>> {
+
    fn memory<'a>(store: &'a MockRepository) -> Cache<'a, MockRepository, ReadOnly, Store<Write>> {
+
        let store = super::super::Patches::open(store, ReadOnly).unwrap();
        let cache = Store::<Write>::memory()
            .unwrap()
            .with_migrations(migrate::ignore)
@@ -764,7 +775,7 @@ mod tests {
    #[test]
    fn test_is_empty() {
        let repo = arbitrary::r#gen::<MockRepository>(1);
-
        let mut cache = memory(repo);
+
        let mut cache = memory(&repo);
        assert!(cache.is_empty().unwrap());

        let patch = Patch::new(
@@ -792,7 +803,7 @@ mod tests {
    #[test]
    fn test_counts() {
        let repo = arbitrary::r#gen::<MockRepository>(1);
-
        let mut cache = memory(repo);
+
        let mut cache = memory(&repo);
        let n_open = arbitrary::r#gen::<u8>(0);
        let n_draft = arbitrary::r#gen::<u8>(1);
        let n_archived = arbitrary::r#gen::<u8>(1);
@@ -880,7 +891,7 @@ mod tests {
    #[test]
    fn test_get() {
        let repo = arbitrary::r#gen::<MockRepository>(1);
-
        let mut cache = memory(repo);
+
        let mut cache = memory(&repo);
        let ids = (0..arbitrary::r#gen::<u8>(1))
            .map(|_| PatchId::from(arbitrary::oid()))
            .collect::<BTreeSet<PatchId>>();
@@ -916,7 +927,7 @@ mod tests {
    #[test]
    fn test_find_by_revision() {
        let repo = arbitrary::r#gen::<MockRepository>(1);
-
        let mut cache = memory(repo);
+
        let mut cache = memory(&repo);
        let patch_id = PatchId::from(arbitrary::oid());
        let revisions = (0..arbitrary::r#gen::<NonZeroU8>(1).into())
            .map(|_| revision())
@@ -958,7 +969,7 @@ mod tests {
    #[test]
    fn test_list() {
        let repo = arbitrary::r#gen::<MockRepository>(1);
-
        let mut cache = memory(repo);
+
        let mut cache = memory(&repo);
        let ids = (0..arbitrary::r#gen::<u8>(1))
            .map(|_| PatchId::from(arbitrary::oid()))
            .collect::<BTreeSet<PatchId>>();
@@ -989,7 +1000,7 @@ mod tests {
    #[test]
    fn test_list_by_status() {
        let repo = arbitrary::r#gen::<MockRepository>(1);
-
        let mut cache = memory(repo);
+
        let mut cache = memory(&repo);
        let ids = (0..arbitrary::r#gen::<u8>(1))
            .map(|_| PatchId::from(arbitrary::oid()))
            .collect::<BTreeSet<PatchId>>();
@@ -1020,7 +1031,7 @@ mod tests {
    #[test]
    fn test_remove() {
        let repo = arbitrary::r#gen::<MockRepository>(1);
-
        let mut cache = memory(repo);
+
        let mut cache = memory(&repo);
        let ids = (0..arbitrary::r#gen::<u8>(1))
            .map(|_| PatchId::from(arbitrary::oid()))
            .collect::<BTreeSet<PatchId>>();
modified crates/radicle/src/cob/store.rs
@@ -11,12 +11,14 @@ use serde::{Deserialize, Serialize};
use crate::cob::op::Op;
use crate::cob::{Create, Embed, EntryId, ObjectId, TypeName, Update, Updated, Uri, Version};
use crate::git;
-
use crate::node::device::Device;
use crate::prelude::*;
-
use crate::storage::SignRepository;
use crate::storage::git as storage;
+
use crate::storage::{HasRepoId, SignRepository};
use crate::{cob, identity};

+
pub mod access;
+
use access::WriteAs;
+

pub trait CobAction {
    /// Parent objects this action depends on. For example, patch revisions
    /// have the commit objects as their parent.
@@ -135,94 +137,147 @@ pub enum Error {
    ClashingIdentifiers(String, String),
}

-
/// Storage for collaborative objects of a specific type `T` in a single repository.
-
pub struct Store<'a, T, R> {
-
    identity: Option<git::Oid>,
-
    repo: &'a R,
+
/// The [`Scope`] of a [`Store`] keeps track of what collaborative object is
+
/// being accessed.
+
///
+
/// For example, a `Scope<Patch>` keeps track of `Patch` access.
+
///
+
/// The type parameter `T` is the collaborative object type.
+
pub struct Scope<'a, T> {
    witness: PhantomData<T>,
    type_name: &'a TypeName,
}

-
impl<T, R> AsRef<R> for Store<'_, T, R> {
-
    fn as_ref(&self) -> &R {
+
impl<'a, T: CobWithType> Scope<'a, T> {
+
    fn new() -> Self {
+
        Self {
+
            witness: PhantomData,
+
            type_name: T::type_name(),
+
        }
+
    }
+
}
+

+
/// Storage for collaborative objects of a specific type `T` in a single repository.
+
pub struct Store<'a, T, Repo, Access> {
+
    identity: Option<git::Oid>,
+
    repo: &'a Repo,
+
    scope: Scope<'a, T>,
+
    access: Access,
+
}
+

+
impl<T, Repo, Access> HasRepoId for Store<'_, T, Repo, Access>
+
where
+
    Repo: HasRepoId,
+
{
+
    fn rid(&self) -> RepoId {
+
        self.repo.rid()
+
    }
+
}
+

+
impl<T, Repo, Access> AsRef<Repo> for Store<'_, T, Repo, Access> {
+
    fn as_ref(&self) -> &Repo {
        self.repo
    }
}

-
impl<'a, T, R> Store<'a, T, R>
+
impl<'a, T, Repo, Access> Store<'a, T, Repo, Access>
where
-
    R: ReadRepository + cob::Store,
+
    Repo: ReadRepository + cob::Store<Namespace = NodeId>,
+
    Access: access::Access,
{
-
    /// Open a new generic store.
-
    pub fn open_for(type_name: &'a TypeName, repo: &'a R) -> Result<Self, Error> {
+
    pub fn open_for(
+
        type_name: &'a TypeName,
+
        repo: &'a Repo,
+
        access: Access,
+
    ) -> Result<Self, Error> {
        Ok(Self {
            repo,
            identity: None,
-
            witness: PhantomData,
-
            type_name,
+
            scope: Scope {
+
                witness: PhantomData,
+
                type_name,
+
            },
+
            access,
        })
    }
+
}

+
impl<'a, T, Repo, Access> Store<'a, T, Repo, Access>
+
where
+
    Repo: ReadRepository + cob::Store<Namespace = NodeId>,
+
    Access: access::Access,
+
{
    /// Return a new store with the attached identity.
    pub fn identity(self, identity: git::Oid) -> Self {
        Self {
            repo: self.repo,
-
            witness: self.witness,
            identity: Some(identity),
-
            type_name: self.type_name,
+
            scope: self.scope,
+
            access: self.access,
        }
    }
}

-
impl<'a, T, R> Store<'a, T, R>
+
impl<'a, T, Repo, Access> Store<'a, T, Repo, Access>
where
-
    R: ReadRepository + cob::Store<Namespace = NodeId>,
    T: CobWithType,
+
    Repo: ReadRepository + cob::Store<Namespace = NodeId>,
+
    Access: access::Access,
{
    /// Open a new generic store.
-
    pub fn open(repo: &'a R) -> Result<Self, Error> {
+
    pub fn open(repo: &'a Repo, access: Access) -> Result<Self, Error> {
        Ok(Self {
            repo,
            identity: None,
-
            witness: PhantomData,
-
            type_name: T::type_name(),
+
            scope: Scope::new(),
+
            access,
        })
    }
}

-
impl<T, R> Store<'_, T, R>
+
impl<'a, 'b, T, Repo, Signer> Store<'a, T, Repo, WriteAs<'b, Signer>>
where
-
    R: ReadRepository + cob::Store<Namespace = NodeId>,
-
    T: Cob + cob::Evaluate<R>,
+
    T: Cob + cob::Evaluate<Repo>,
+
    Repo: ReadRepository + cob::Store<Namespace = NodeId>,
{
+
    #[deprecated(note = "only exists to accommodate signatures in crate::cob::identity")]
+
    pub(super) fn signer(&self) -> &Signer {
+
        self.access.signer
+
    }
+

+
    #[deprecated(note = "only exists to accommodate signatures in crate::cob::identity")]
+
    pub(super) fn repo(&self) -> &Repo {
+
        self.repo
+
    }
+

    pub fn transaction(
        &self,
        actions: Vec<T::Action>,
        embeds: Vec<Embed<Uri>>,
-
    ) -> Transaction<T, R> {
-
        Transaction::new(self.type_name.clone(), actions, embeds)
+
    ) -> Transaction<T, Repo> {
+
        Transaction::new(self.scope.type_name.clone(), actions, embeds)
    }
}

-
impl<T, R> Store<'_, T, R>
+
impl<'a, 'b, T, Repo, Signer> Store<'a, T, Repo, WriteAs<'b, Signer>>
where
-
    R: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
-
    T: Cob + cob::Evaluate<R>,
+
    T: Cob + cob::Evaluate<Repo>,
    T::Action: Serialize,
+
    Repo: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
+
    Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
    Signer: crypto::signature::Signer<crypto::Signature>,
+
    Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
+
    Signer: crypto::signature::Verifier<crypto::Signature>,
{
    /// Update an object.
-
    pub fn update<G>(
-
        &self,
+
    pub fn update(
+
        &mut self,
        type_name: &TypeName,
        object_id: ObjectId,
        message: &str,
        actions: impl Into<NonEmpty<T::Action>>,
        embeds: Vec<Embed<Uri>>,
-
        signer: &Device<G>,
-
    ) -> Result<Updated<T>, Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
+
    ) -> Result<Updated<T>, Error> {
        let actions = actions.into();
        let related = actions.iter().flat_map(T::Action::parents).collect();
        let changes = actions.try_map(encoding::encode)?;
@@ -235,12 +290,13 @@ where
                })
            })
            .collect::<Result<_, _>>()?;
+
        let namespace = self.access.signer.verifying_key();
        let updated = cob::update(
            self.repo,
-
            signer,
+
            self.access.signer,
            self.identity,
            related,
-
            signer.public_key(),
+
            &namespace,
            Update {
                object_id,
                type_name: type_name.clone(),
@@ -250,23 +306,19 @@ where
            },
        )?;
        self.repo
-
            .sign_refs(signer)
+
            .sign_refs(self.access.signer)
            .map_err(|e| Error::SignRefs(Box::new(e)))?;

        Ok(updated)
    }

    /// Create an object.
-
    pub fn create<G>(
-
        &self,
+
    pub fn create(
+
        &mut self,
        message: &str,
        actions: impl Into<NonEmpty<T::Action>>,
        embeds: Vec<Embed<Uri>>,
-
        signer: &Device<G>,
-
    ) -> Result<(ObjectId, T), Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
+
    ) -> Result<(ObjectId, T), Error> {
        let actions = actions.into();
        let parents = actions.iter().flat_map(T::Action::parents).collect();
        let contents = actions.try_map(encoding::encode)?;
@@ -279,14 +331,15 @@ where
                })
            })
            .collect::<Result<_, _>>()?;
+
        let namespace = self.access.signer.verifying_key();
        let cob = cob::create::<T, _, _>(
            self.repo,
-
            signer,
+
            self.access.signer,
            self.identity,
            parents,
-
            signer.public_key(),
+
            &namespace,
            Create {
-
                type_name: self.type_name.clone(),
+
                type_name: self.scope.type_name.clone(),
                version: Version::default(),
                message: message.to_owned(),
                embeds,
@@ -296,28 +349,23 @@ where
        // Nb. We can't sign our refs before the identity refs exist, which are created after
        // the identity COB is created. Therefore we manually sign refs when creating identity
        // COBs.
-
        if self.type_name != &*crate::cob::identity::TYPENAME {
+
        if self.scope.type_name != &*crate::cob::identity::TYPENAME {
            self.repo
-
                .sign_refs(signer)
+
                .sign_refs(self.access.signer)
                .map_err(|e| Error::SignRefs(Box::new(e)))?;
        }
        Ok((*cob.id(), cob.object))
    }

    /// Remove an object.
-
    pub fn remove<G>(&self, id: &ObjectId, signer: &Device<G>) -> Result<(), Error>
-
    where
-
        G: crypto::signature::Signer<crypto::Signature>,
-
    {
-
        let name = git::refs::storage::cob(signer.public_key(), self.type_name, id);
-
        match self
-
            .repo
-
            .reference_oid(signer.public_key(), &name.strip_namespace())
-
        {
+
    pub fn remove(&mut self, id: &ObjectId) -> Result<(), Error> {
+
        let namespace = self.access.signer.verifying_key();
+
        let name = git::refs::storage::cob(&namespace, self.scope.type_name, id);
+
        match self.repo.reference_oid(&namespace, &name.strip_namespace()) {
            Ok(_) => {
-
                cob::remove(self.repo, signer.public_key(), self.type_name, id)?;
+
                cob::remove(self.repo, &namespace, self.scope.type_name, id)?;
                self.repo
-
                    .sign_refs(signer)
+
                    .sign_refs(self.access.signer)
                    .map_err(|e| Error::SignRefs(Box::new(e)))?;
                Ok(())
            }
@@ -330,15 +378,16 @@ where
    }
}

-
impl<'a, T, R> Store<'a, T, R>
+
impl<'a, T, Repo, Access> Store<'a, T, Repo, Access>
where
-
    R: ReadRepository + cob::Store,
-
    T: Cob + cob::Evaluate<R> + 'a,
+
    T: Cob + cob::Evaluate<Repo> + 'a,
    T::Action: Serialize,
+
    Repo: ReadRepository + cob::Store<Namespace = NodeId>,
+
    Access: access::Access,
{
    /// Get an object.
    pub fn get(&self, id: &ObjectId) -> Result<Option<T>, Error> {
-
        cob::get::<T, _>(self.repo, self.type_name, id)
+
        cob::get::<T, _>(self.repo, self.scope.type_name, id)
            .map(|r| r.map(|cob| cob.object))
            .map_err(Error::from)
    }
@@ -346,9 +395,11 @@ where
    /// Return all objects.
    pub fn all(
        &self,
-
    ) -> Result<impl ExactSizeIterator<Item = Result<(ObjectId, T), Error>> + use<'a, T, R>, Error>
-
    {
-
        let raw = cob::list::<T, _>(self.repo, self.type_name)?;
+
    ) -> Result<
+
        impl ExactSizeIterator<Item = Result<(ObjectId, T), Error>> + use<'a, T, Repo, Access>,
+
        Error,
+
    > {
+
        let raw = cob::list::<T, _>(self.repo, self.scope.type_name)?;

        Ok(raw.into_iter().map(|o| Ok((*o.id(), o.object))))
    }
@@ -360,7 +411,7 @@ where

    /// Return objects count.
    pub fn count(&self) -> Result<usize, Error> {
-
        let raw = cob::list::<T, _>(self.repo, self.type_name)?;
+
        let raw = cob::list::<T, _>(self.repo, self.scope.type_name)?;

        Ok(raw.len())
    }
@@ -408,26 +459,33 @@ where
            type_name,
        }
    }
+

+
    #[deprecated(note = "only exists to accommodate signatures in crate::cob::identity")]
+
    pub(super) fn into_inner(self) -> (Vec<T::Action>, Vec<Embed<Uri>>) {
+
        (self.actions, self.embeds)
+
    }
}

-
impl<T, R> Transaction<T, R>
+
impl<T, Repo> Transaction<T, Repo>
where
-
    T: Cob + CobWithType + cob::Evaluate<R>,
+
    T: Cob + CobWithType + cob::Evaluate<Repo>,
{
    /// Create a new transaction to be used as the initial set of operations for a COB.
-
    pub fn initial<G, F, Tx>(
+
    pub fn initial<'a, 'b, Signer, Tx, F>(
        message: &str,
-
        store: &mut Store<T, R>,
-
        signer: &Device<G>,
+
        store: &mut Store<'a, T, Repo, WriteAs<'b, Signer>>,
        operations: F,
    ) -> Result<(ObjectId, T), Error>
    where
+
        T::Action: Serialize + Clone,
+
        Repo: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
+
        Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
        Signer: crypto::signature::Signer<crypto::Signature>,
+
        Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
+
        Signer: crypto::signature::Verifier<crypto::Signature>,
        Tx: From<Self>,
        Self: From<Tx>,
-
        G: crypto::signature::Signer<crypto::Signature>,
-
        F: FnOnce(&mut Tx, &R) -> Result<(), Error>,
-
        R: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
-
        T::Action: Serialize + Clone,
+
        F: FnOnce(&mut Tx, &Repo) -> Result<(), Error>,
    {
        let mut tx = Tx::from(Transaction::default());
        operations(&mut tx, store.as_ref())?;
@@ -436,13 +494,13 @@ where
        let actions = NonEmpty::from_vec(tx.actions)
            .expect("Transaction::initial: transaction must contain at least one action");

-
        store.create(message, actions, tx.embeds, signer)
+
        store.create(message, actions, tx.embeds)
    }
}

-
impl<T, R> Transaction<T, R>
+
impl<T, Repo> Transaction<T, Repo>
where
-
    T: Cob + cob::Evaluate<R>,
+
    T: Cob + cob::Evaluate<Repo>,
{
    /// Add an action to this transaction.
    pub fn push(&mut self, action: T::Action) -> Result<(), Error> {
@@ -482,25 +540,28 @@ where
    /// Commit transaction.
    ///
    /// Returns an operation that can be applied onto an in-memory state.
-
    pub fn commit<G>(
+
    pub fn commit<'a, Signer>(
        self,
        msg: &str,
        id: ObjectId,
-
        store: &mut Store<T, R>,
-
        signer: &Device<G>,
+
        store: &mut Store<T, Repo, WriteAs<'a, Signer>>,
    ) -> Result<(T, EntryId), Error>
    where
-
        R: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
        T::Action: Serialize + Clone,
-
        G: crypto::signature::Signer<crypto::Signature>,
+
        Repo: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
+
        Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
        Signer: crypto::signature::Signer<crypto::Signature>,
+
        Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
+
        Signer: crypto::signature::Verifier<crypto::Signature>,
    {
        let actions = NonEmpty::from_vec(self.actions)
            .expect("Transaction::commit: transaction must not be empty");
+

        let Updated {
            head,
            object: CollaborativeObject { object, .. },
            ..
-
        } = store.update(&self.type_name, id, msg, actions, self.embeds, signer)?;
+
        } = store.update(&self.type_name, id, msg, actions, self.embeds)?;

        Ok((object, head))
    }
added crates/radicle/src/cob/store/access.rs
@@ -0,0 +1,37 @@
+
//! Stores carry [`Access`] to indicate allowed access modes. In particular
+
//! whether writes to the store are allowed.
+

+
pub use seal::Access;
+

+
/// [`ReadOnly`] is used for read-only [`Access`].
+
pub struct ReadOnly;
+

+
/// [`WriteAs`] is used for write [`Access`].
+
pub struct WriteAs<'a, Signer> {
+
    pub(super) signer: &'a Signer,
+
}
+

+
impl<'a, Signer> WriteAs<'a, Signer> {
+
    pub fn new(signer: &'a Signer) -> Self {
+
        Self { signer }
+
    }
+
}
+

+
// See <https://predr.ag/blog/definitive-guide-to-sealed-traits-in-rust/#sealing-traits-via-method-signatures>.
+
#[allow(private_interfaces)]
+
mod seal {
+
    enum Seal {}
+

+
    /// Marker trait for COB store access modes.
+
    pub trait Access {
+
        fn seal(&self, _: Seal);
+
    }
+

+
    impl Access for super::ReadOnly {
+
        fn seal(&self, _: Seal) {}
+
    }
+

+
    impl<Signer> Access for super::WriteAs<'_, Signer> {
+
        fn seal(&self, _: Seal) {}
+
    }
+
}
modified crates/radicle/src/node/device.rs
@@ -204,3 +204,12 @@ impl AsRef<crypto::PublicKey> for BoxedDevice {
impl KeypairRef for BoxedDevice {
    type VerifyingKey = crypto::PublicKey;
}
+

+
impl Verifier<Signature> for BoxedDevice {
+
    fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), crypto::signature::Error> {
+
        self.0
+
            .node
+
            .verify(msg, signature)
+
            .map_err(crypto::signature::Error::from_source)
+
    }
+
}
modified crates/radicle/src/profile.rs
@@ -22,6 +22,7 @@ use localtime::LocalTime;
use thiserror::Error;

use crate::cob::migrate;
+
use crate::cob::store::access::{ReadOnly, WriteAs};
use crate::crypto::PublicKey;
use crate::crypto::ssh::agent::Agent;
use crate::crypto::ssh::{Keystore, Passphrase, keystore};
@@ -757,15 +758,16 @@ impl Home {
    }

    /// Return a read-only handle for the issues cache.
-
    pub fn issues<'a, R>(
+
    pub fn issues<'a, Repo>(
        &self,
-
        repository: &'a R,
-
    ) -> Result<cob::issue::Cache<cob::issue::Issues<'a, R>, cob::cache::StoreReader>, Error>
+
        repository: &'a Repo,
+
    ) -> Result<cob::issue::Cache<'a, Repo, ReadOnly, cob::cache::StoreReader>, Error>
    where
-
        R: ReadRepository + cob::Store<Namespace = NodeId>,
+
        Repo: ReadRepository + cob::Store<Namespace = NodeId>,
    {
        let db = self.cobs_db()?;
-
        let store = cob::issue::Issues::open(repository)?;
+

+
        let store = cob::issue::Issues::open(repository, ReadOnly)?;

        db.check_version()?;

@@ -773,15 +775,16 @@ impl Home {
    }

    /// Return a read-write handle for the issues cache.
-
    pub fn issues_mut<'a, R>(
+
    pub fn issues_mut<'a, 'b, Repo, Signer>(
        &self,
-
        repository: &'a R,
-
    ) -> Result<cob::issue::Cache<cob::issue::Issues<'a, R>, cob::cache::StoreWriter>, Error>
+
        repository: &'a Repo,
+
        signer: &'b Signer,
+
    ) -> Result<cob::issue::Cache<'a, Repo, WriteAs<'b, Signer>, cob::cache::StoreWriter>, Error>
    where
-
        R: ReadRepository + cob::Store<Namespace = NodeId>,
+
        Repo: ReadRepository + cob::Store<Namespace = NodeId>,
    {
        let db = self.cobs_db_mut()?;
-
        let store = cob::issue::Issues::open(repository)?;
+
        let store = cob::issue::Issues::open(repository, WriteAs::new(signer))?;

        db.check_version()?;

@@ -789,15 +792,15 @@ impl Home {
    }

    /// Return a read-only handle for the patches cache.
-
    pub fn patches<'a, R>(
+
    pub fn patches<'a, Repo>(
        &self,
-
        repository: &'a R,
-
    ) -> Result<cob::patch::Cache<cob::patch::Patches<'a, R>, cob::cache::StoreReader>, Error>
+
        repository: &'a Repo,
+
    ) -> Result<cob::patch::Cache<'a, Repo, ReadOnly, cob::cache::StoreReader>, Error>
    where
-
        R: ReadRepository + cob::Store<Namespace = NodeId>,
+
        Repo: ReadRepository + cob::Store<Namespace = NodeId>,
    {
        let db = self.cobs_db()?;
-
        let store = cob::patch::Patches::open(repository)?;
+
        let store = cob::patch::Patches::open(repository, ReadOnly)?;

        db.check_version()?;

@@ -805,15 +808,16 @@ impl Home {
    }

    /// Return a read-write handle for the patches cache.
-
    pub fn patches_mut<'a, R>(
+
    pub fn patches_mut<'a, 'b, Repo, Signer>(
        &self,
-
        repository: &'a R,
-
    ) -> Result<cob::patch::Cache<cob::patch::Patches<'a, R>, cob::cache::StoreWriter>, Error>
+
        repository: &'a Repo,
+
        signer: &'b Signer,
+
    ) -> Result<cob::patch::Cache<'a, Repo, WriteAs<'b, Signer>, cob::cache::StoreWriter>, Error>
    where
-
        R: ReadRepository + cob::Store<Namespace = NodeId>,
+
        Repo: ReadRepository + cob::Store<Namespace = NodeId>,
    {
        let db = self.cobs_db_mut()?;
-
        let store = cob::patch::Patches::open(repository)?;
+
        let store = cob::patch::Patches::open(repository, WriteAs::new(signer))?;

        db.check_version()?;

modified crates/radicle/src/rad.rs
@@ -76,6 +76,7 @@ where
        )
    })?;
    let doc = identity::Doc::initial(proj, delegate, visibility);
+

    let (project, identity) = Repository::init(&doc, &storage, signer)?;
    let url = git::Url::from(project.id);

modified crates/radicle/src/storage.rs
@@ -24,7 +24,6 @@ use crate::identity::{Did, PayloadError, doc};
use crate::identity::{Doc, DocAt, DocError};
use crate::identity::{Identity, RepoId};
use crate::node::SyncedAt;
-
use crate::node::device::Device;
use crate::storage::git::NAMESPACES_GLOB;
use crate::storage::refs::{FeatureLevel, Refs, SignedRefs};

@@ -697,16 +696,20 @@ pub trait WriteRepository: ReadRepository + SignRepository {
/// Allows signing refs.
pub trait SignRepository {
    /// Sign the repository's refs under the `refs/rad/sigrefs` branch.
-
    fn sign_refs<G>(&self, signer: &Device<G>) -> Result<SignedRefs, RepositoryError>
+
    fn sign_refs<Signer>(&self, signer: &Signer) -> Result<SignedRefs, RepositoryError>
    where
-
        G: crypto::signature::Signer<crypto::Signature>;
+
        Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
        Signer: crypto::signature::Signer<crypto::Signature>,
+
        Signer: crypto::signature::Verifier<crypto::Signature>;

    /// Sign the repository's refs under the `refs/rad/sigrefs` branch, even if unchanged.
    ///
    /// Most users will prefer [`Self::sign_refs`].
-
    fn force_sign_refs<G>(&self, signer: &Device<G>) -> Result<SignedRefs, RepositoryError>
+
    fn force_sign_refs<Signer>(&self, signer: &Signer) -> Result<SignedRefs, RepositoryError>
    where
-
        G: crypto::signature::Signer<crypto::Signature>;
+
        Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
        Signer: crypto::signature::Signer<crypto::Signature>,
+
        Signer: crypto::signature::Verifier<crypto::Signature>;
}

impl<T, S> ReadStorage for T
modified crates/radicle/src/storage/git.rs
@@ -1004,43 +1004,52 @@ impl WriteRepository for Repository {
}

impl SignRepository for Repository {
-
    fn sign_refs<G: crypto::signature::Signer<crypto::Signature>>(
-
        &self,
-
        signer: &Device<G>,
-
    ) -> Result<SignedRefs, RepositoryError> {
+
    fn sign_refs<Signer>(&self, signer: &Signer) -> Result<SignedRefs, RepositoryError>
+
    where
+
        Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
        Signer: crypto::signature::Signer<crypto::Signature>,
+
        Signer: crypto::signature::Verifier<crypto::Signature>,
+
    {
        self.sign_refs_with(signer, false)
    }

-
    fn force_sign_refs<G: crypto::signature::Signer<crypto::Signature>>(
-
        &self,
-
        signer: &Device<G>,
-
    ) -> Result<SignedRefs, RepositoryError> {
+
    fn force_sign_refs<Signer>(&self, signer: &Signer) -> Result<SignedRefs, RepositoryError>
+
    where
+
        Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
        Signer: crypto::signature::Signer<crypto::Signature>,
+
        Signer: crypto::signature::Verifier<crypto::Signature>,
+
    {
        self.sign_refs_with(signer, true)
    }
}

impl Repository {
-
    fn sign_refs_with<G: crypto::signature::Signer<crypto::Signature>>(
+
    fn sign_refs_with<Signer>(
        &self,
-
        signer: &Device<G>,
+
        signer: &Signer,
        force: bool,
-
    ) -> Result<SignedRefs, RepositoryError> {
-
        let remote = signer.public_key();
+
    ) -> Result<SignedRefs, RepositoryError>
+
    where
+
        Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
        Signer: crypto::signature::Signer<crypto::Signature>,
+
        Signer: crypto::signature::Verifier<crypto::Signature>,
+
    {
+
        let remote = signer.verifying_key();
        // Ensure the root reference is set, which is checked during sigref verification.
        if self
-
            .reference_oid(remote, &git::refs::storage::IDENTITY_ROOT)
+
            .reference_oid(&remote, &git::refs::storage::IDENTITY_ROOT)
            .is_err()
        {
-
            self.set_remote_identity_root(remote)?;
+
            self.set_remote_identity_root(&remote)?;
        }

-
        let committer = refs::sigrefs::git::Committer::from_env_or_now(remote);
+
        let committer = refs::sigrefs::git::Committer::from_env_or_now(&remote);

-
        let refs = self.references_of(remote)?;
+
        let refs = self.references_of(&remote)?;
        let signed = if force {
-
            refs.force_save(*remote, committer, self, signer)?
+
            refs.force_save(remote, committer, self, signer)?
        } else {
-
            refs.save(*remote, committer, self, signer)?
+
            refs.save(remote, committer, self, signer)?
        };

        Ok(signed)
modified crates/radicle/src/storage/git/cob.rs
@@ -17,7 +17,6 @@ use crate::git::*;
use crate::identity;
use crate::identity::doc::DocError;
use crate::node::NodeId;
-
use crate::node::device::Device;
use crate::storage;
use crate::storage::Error;
use crate::storage::{
@@ -232,25 +231,35 @@ impl<R: storage::WriteRepository> change::Storage for DraftStore<'_, R> {
    }
}

-
impl<R> SignRepository for DraftStore<'_, R>
+
impl<Repo> SignRepository for DraftStore<'_, Repo>
where
-
    R: storage::ReadRepository,
+
    Repo: storage::ReadRepository,
{
-
    fn sign_refs<G: crypto::signature::Signer<crypto::Signature>>(
+
    fn sign_refs<Signer>(
        &self,
-
        signer: &Device<G>,
-
    ) -> Result<storage::refs::SignedRefs, RepositoryError> {
+
        signer: &Signer,
+
    ) -> Result<storage::refs::SignedRefs, RepositoryError>
+
    where
+
        Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
        Signer: crypto::signature::Signer<crypto::Signature>,
+
        Signer: crypto::signature::Verifier<crypto::Signature>,
+
    {
        // Since this is a draft store, we do not actually want to sign the refs.
        // Instead, we just return the existing signed refs.
-
        let remote = self.repo.remote(signer.public_key())?;
+
        let remote = self.repo.remote(&signer.verifying_key())?;

        Ok(remote.refs)
    }

-
    fn force_sign_refs<G: crypto::signature::Signer<crypto::Signature>>(
+
    fn force_sign_refs<Signer>(
        &self,
-
        signer: &Device<G>,
-
    ) -> Result<storage::refs::SignedRefs, RepositoryError> {
+
        signer: &Signer,
+
    ) -> Result<storage::refs::SignedRefs, RepositoryError>
+
    where
+
        Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
        Signer: crypto::signature::Signer<crypto::Signature>,
+
        Signer: crypto::signature::Verifier<crypto::Signature>,
+
    {
        self.sign_refs(signer)
    }
}
modified crates/radicle/src/storage/refs.rs
@@ -499,6 +499,7 @@ mod tests {

    use super::*;
    use crate::assert_matches;
+

    use crate::node::device::Device;
    use crate::storage::WriteRepository as _;
    use crate::{Storage, cob::Title, cob::identity::Identity, rad, test::fixtures};
@@ -574,14 +575,14 @@ mod tests {
                })
                .unwrap();

-
            let mut paris_ident = Identity::load_mut(&paris).unwrap();
-
            let mut london_ident = Identity::load_mut(&london).unwrap();
+
            let mut paris_ident = Identity::load_mut(&paris, &alice).unwrap();
+
            let mut london_ident = Identity::load_mut(&london, &alice).unwrap();

            paris_ident
-
                .update(Title::new("Add Bob").unwrap(), "", &paris_doc, &alice)
+
                .update(Title::new("Add Bob").unwrap(), "", &paris_doc)
                .unwrap();
            london_ident
-
                .update(Title::new("Add Bob").unwrap(), "", &london_doc, &alice)
+
                .update(Title::new("Add Bob").unwrap(), "", &london_doc)
                .unwrap();
        }

modified crates/radicle/src/test/storage.rs
@@ -11,7 +11,6 @@ use crate::git::fmt;

use crate::identity::doc::{Doc, DocAt, DocError, RawDoc, RepoId};
use crate::node::NodeId;
-
use crate::node::device::Device;

pub use crate::storage::*;

@@ -367,17 +366,23 @@ impl WriteRepository for MockRepository {
}

impl SignRepository for MockRepository {
-
    fn sign_refs<G: crypto::signature::Signer<crypto::Signature>>(
+
    fn sign_refs<Signer>(
        &self,
-
        _signer: &Device<G>,
-
    ) -> Result<crate::storage::refs::SignedRefs, RepositoryError> {
+
        _signer: &Signer,
+
    ) -> Result<crate::storage::refs::SignedRefs, RepositoryError>
+
    where
+
        Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
        Signer: crypto::signature::Signer<crypto::Signature>,
+
    {
        todo!()
    }

-
    fn force_sign_refs<G: crypto::signature::Signer<crypto::Signature>>(
-
        &self,
-
        _signer: &Device<G>,
-
    ) -> Result<crate::storage::refs::SignedRefs, RepositoryError> {
+
    fn force_sign_refs<Signer>(&self, _signer: &Signer) -> Result<refs::SignedRefs, RepositoryError>
+
    where
+
        Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
+
        Signer: crypto::signature::Signer<crypto::Signature>,
+
        Signer: crypto::signature::Verifier<crypto::Signature>,
+
    {
        todo!()
    }
}