Conversation Sharing

Conversation Sharing

This guide shows you how to implement conversation sharing and ownership transfer features in your Quarkus application using the Memory Service.

đź’ˇ New to sharing concepts? Read Sharing & Access Control first to understand access levels, membership management, and ownership transfers. This guide focuses on the Quarkus implementation.

Prerequisites

Starting checkpoint: This guide starts from java/quarkus/examples/doc-checkpoints/05-response-resumption

Make sure you’ve completed the previous guides:

Membership Endpoints

Add the membership endpoints to your ConversationsResource:

ConversationsResource.java
    // Membership management endpoints
    @GET
    @Path("/{conversationId}/memberships")
    @Produces(MediaType.APPLICATION_JSON)
    public Response listConversationMemberships(
            @PathParam("conversationId") String conversationId) {
        return proxy.listConversationMemberships(conversationId, null, null);
    }

    @POST
    @Path("/{conversationId}/memberships")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response shareConversation(
            @PathParam("conversationId") String conversationId, String body) {
        return proxy.shareConversation(conversationId, body);
    }

    @PATCH
    @Path("/{conversationId}/memberships/{userId}")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response updateConversationMembership(
            @PathParam("conversationId") String conversationId,
            @PathParam("userId") String userId,
            String body) {
        return proxy.updateConversationMembership(conversationId, userId, body);
    }

    @DELETE
    @Path("/{conversationId}/memberships/{userId}")
    public Response deleteConversationMembership(
            @PathParam("conversationId") String conversationId,
            @PathParam("userId") String userId) {
        return proxy.deleteConversationMembership(conversationId, userId);
    }

Why: These endpoints let your frontend implement a sharing UI without directly exposing the Memory Service. The proxy pattern ensures that all calls carry both the service account API key (for Memory Service authentication) and the caller’s Bearer token (for authorization checks), so users can only manage memberships on conversations they own or manage.

Ownership Transfer Endpoints

Create a new OwnershipTransfersResource:

OwnershipTransfersResource.java
package org.acme;

import io.github.chirino.memory.runtime.MemoryServiceProxy;
import io.smallrye.common.annotation.Blocking;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;

@Path("/v1/ownership-transfers")
@ApplicationScoped
@Blocking
public class OwnershipTransfersResource {

    @Inject MemoryServiceProxy proxy;

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Response listPendingTransfers(@QueryParam("role") String role) {
        return proxy.listPendingTransfers(role, null, null);
    }

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response createOwnershipTransfer(String body) {
        return proxy.createOwnershipTransfer(body);
    }

    @GET
    @Path("/{transferId}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getTransfer(@PathParam("transferId") String transferId) {
        return proxy.getTransfer(transferId);
    }

    @POST
    @Path("/{transferId}/accept")
    @Produces(MediaType.APPLICATION_JSON)
    public Response acceptTransfer(@PathParam("transferId") String transferId) {
        return proxy.acceptTransfer(transferId);
    }

    @DELETE
    @Path("/{transferId}")
    public Response deleteTransfer(@PathParam("transferId") String transferId) {
        return proxy.deleteTransfer(transferId);
    }
}

Why: Ownership transfer is a two-step process — the current owner initiates it and the recipient must explicitly accept. Keeping this in its own resource class separates membership management (granting access) from ownership transfer (changing who is ultimately responsible for a conversation), making each concern easier to reason about and audit.

Testing Membership Management

Setting Up Helper Functions

First, create a helper function to get access tokens for different users:

# Get access token for a user
function get-token() {
  local username=${1:-bob}
  local password=${2:-$username}
  curl -sSfX POST http://localhost:8081/realms/memory-service/protocol/openid-connect/token \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "client_id=memory-service-client" \
    -d "client_secret=change-me" \
    -d "grant_type=password" \
    -d "username=$username" \
    -d "password=$password" \
    | jq -r '.access_token'
}

First, create a conversation as bob so the membership endpoints have data to work with:

curl -sSfX POST http://localhost:9090/chat/4f65f1b4-d0fb-41ab-b8f9-0aedcc721103 \
  -H "Content-Type: text/plain" \
  -H "Authorization: Bearer $(get-token)" \
  -d "Hello, starting a conversation for sharing tests."

Example output:

Hello! I'm ready to help with your sharing tests. What would you like to discuss?

List Members

# As bob (owner), list members of the conversation
curl -sSfX GET http://localhost:9090/v1/conversations/4f65f1b4-d0fb-41ab-b8f9-0aedcc721103/memberships \
  -H "Authorization: Bearer $(get-token bob bob)" | jq

Example output:

{
  "data": [
    {
      "conversationId": "4f65f1b4-d0fb-41ab-b8f9-0aedcc721103",
      "userId": "bob",
      "accessLevel": "owner",
      "createdAt": "2026-03-06T14:59:31.839688Z"
    }
  ]
}

Add a Member

# Share conversation with alice as a writer
curl -sSfX POST http://localhost:9090/v1/conversations/4f65f1b4-d0fb-41ab-b8f9-0aedcc721103/memberships \
  -H "Authorization: Bearer $(get-token bob bob)" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "alice",
    "accessLevel": "writer"
  }' | jq

Example output:

{
  "conversationId": "4f65f1b4-d0fb-41ab-b8f9-0aedcc721103",
  "userId": "alice",
  "accessLevel": "writer",
  "createdAt": "2026-03-06T14:59:33.389589Z"
}

Alice can now read and write to the conversation:

curl -sSfX POST http://localhost:9090/chat/4f65f1b4-d0fb-41ab-b8f9-0aedcc721103 \
  -H "Authorization: Bearer $(get-token alice alice)" \
  -H "Content-Type: text/plain" \
  -d "Hi from Alice!"

Example output:

Hi Alice! Great to hear from you. How can I help you today?

Update Access Level

# Promote alice from writer to manager
curl -sSfX PATCH http://localhost:9090/v1/conversations/4f65f1b4-d0fb-41ab-b8f9-0aedcc721103/memberships/alice \
  -H "Authorization: Bearer $(get-token bob bob)" \
  -H "Content-Type: application/json" \
  -d '{
    "accessLevel": "manager"
  }' | jq

Example response:

{
  "conversationId": "4f65f1b4-d0fb-41ab-b8f9-0aedcc721103",
  "userId": "alice",
  "accessLevel": "manager",
  "createdAt": "2025-01-10T14:33:15Z"
}

Now alice can share with others (but only assign writer/reader levels):

curl -sSfX POST http://localhost:9090/v1/conversations/4f65f1b4-d0fb-41ab-b8f9-0aedcc721103/memberships \
  -H "Authorization: Bearer $(get-token alice alice)" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "charlie",
    "accessLevel": "reader"
  }' | jq

Example response:

{
  "conversationId": "4f65f1b4-d0fb-41ab-b8f9-0aedcc721103",
  "userId": "charlie",
  "accessLevel": "reader",
  "createdAt": "2025-01-10T14:34:00Z"
}

Remove a Member

# Remove charlie from the conversation
curl -sSfX DELETE http://localhost:9090/v1/conversations/4f65f1b4-d0fb-41ab-b8f9-0aedcc721103/memberships/charlie \
  -H "Authorization: Bearer $(get-token bob bob)"

Returns 204 No Content on success. Charlie can no longer access the conversation:

curl -sSf GET http://localhost:9090/v1/conversations/4f65f1b4-d0fb-41ab-b8f9-0aedcc721103 \
  -H "Authorization: Bearer $(get-token charlie charlie)"

Returns 403 Forbidden.

Testing Ownership Transfers

Initiate a Transfer

# Bob (owner) wants to transfer ownership to alice
curl -sSfX POST http://localhost:9090/v1/ownership-transfers \
  -H "Authorization: Bearer $(get-token bob bob)" \
  -H "Content-Type: application/json" \
  -d '{
    "conversationId": "4f65f1b4-d0fb-41ab-b8f9-0aedcc721103",
    "newOwnerUserId": "alice"
  }' | jq

Example response:

{
  "id": "17c661b1-aa73-4b1d-91d1-f68c180a0b21",
  "conversationId": "4f65f1b4-d0fb-41ab-b8f9-0aedcc721103",
  "fromUserId": "bob",
  "toUserId": "alice",
  "createdAt": "2025-02-08T10:30:00Z"
}

List Pending Transfers

# Alice sees transfers where she is the recipient
curl -sSfX GET "http://localhost:9090/v1/ownership-transfers?role=recipient" \
  -H "Authorization: Bearer $(get-token alice alice)" | jq

Example response:

{
  "data": [
    {
      "id": "17c661b1-aa73-4b1d-91d1-f68c180a0b21",
      "conversationId": "4f65f1b4-d0fb-41ab-b8f9-0aedcc721103",
      "fromUserId": "bob",
      "toUserId": "alice",
      "createdAt": "2025-02-08T10:30:00Z"
    }
  ]
}

Other role filters:

# Bob sees transfers where he is the sender
curl -sSfX GET "http://localhost:9090/v1/ownership-transfers?role=sender" \
  -H "Authorization: Bearer $(get-token bob bob)" | jq

# List all (sent or received)
curl -sSfX GET "http://localhost:9090/v1/ownership-transfers?role=all" \
  -H "Authorization: Bearer $(get-token alice alice)" | jq

Accept a Transfer

# Alice accepts the transfer (use the transfer ID from the create response)
TRANSFER_ID="17c661b1-aa73-4b1d-91d1-f68c180a0b21"
curl -sSfX POST http://localhost:9090/v1/ownership-transfers/$TRANSFER_ID/accept \
  -H "Authorization: Bearer $(get-token alice alice)"

Returns 204 No Content on success. Verify the new membership state:

curl -sSfX GET http://localhost:9090/v1/conversations/4f65f1b4-d0fb-41ab-b8f9-0aedcc721103/memberships \
  -H "Authorization: Bearer $(get-token alice alice)" | jq

Example response:

{
  "data": [
    {
      "conversationId": "4f65f1b4-d0fb-41ab-b8f9-0aedcc721103",
      "userId": "alice",
      "accessLevel": "owner",
      "createdAt": "2025-01-10T14:33:15Z"
    },
    {
      "conversationId": "4f65f1b4-d0fb-41ab-b8f9-0aedcc721103",
      "userId": "bob",
      "accessLevel": "manager",
      "createdAt": "2025-01-10T14:32:05Z"
    }
  ]
}

Alice is now the owner. Bob was automatically demoted to manager.

Decline/Cancel a Transfer

# Alice can decline the transfer (or bob can cancel it)
TRANSFER_ID="17c661b1-aa73-4b1d-91d1-f68c180a0b21"
curl -sSfX DELETE http://localhost:9090/v1/ownership-transfers/$TRANSFER_ID \
  -H "Authorization: Bearer $(get-token alice alice)"

Returns 204 No Content. The transfer is deleted and ownership remains unchanged.

Completed Checkpoint

Completed code: View the full implementation at java/quarkus/examples/doc-checkpoints/06-sharing