Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
radicle: Refactor COB Storage Access
Merged lorenz opened 1 month ago

Instead of passing the signer as an argument to many methods on Store, scope the Store itself to a signer.

This further allows to differentiate two different access modes on the store in radicle::cob::store::access: WriteAs (which requires signer) and ReadOnly (which does not require a signer).

The caches for issues and patches in radicle::cob::{issue,patch}::Cache are concretised by removing the first type parameter, since it was specific to issues and patches anyway. This was done in this commit as it touches very similar usage sites.

Make Device less prominent, and instead lean more heavily towards traits from the signature crate, such as Keypair and Verifier in addition to Signer. Trait bounds regarding Signer could be simplified, but this is left for the future.

In radicle-cli, the function term::cob::patches_mut, which generates errors with a hint is used instead of the lower-level Profile::patches_mut.

Commands rad issue cache and rad patch cache now construct a writeable cache on top of a read-only store.

Many knock-on changes are handled as well, to arrive at a clean state.

48 files changed +1241 -1290 10a82958 f223afd9
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 crate::cob::identity")]
+
    pub(super) fn signer(&self) -> &Signer {
+
        self.access.signer
+
    }
+

+
    #[deprecated(note = "only exists to accommodate signatures 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 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!()
    }
}