Conversation Sharing
Conversation Sharing
This guide shows you how to implement conversation sharing and ownership transfer features in your Spring Boot 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 Spring Boot implementation.
Prerequisites
Starting checkpoint: This guide starts from java/spring/examples/doc-checkpoints/05-response-resumption
Make sure you’ve completed the previous guides:
- Conversation Forking - Branching conversations
- Response Recording and Resumption - Streaming with resume support
- Multiple test users in Keycloak (bob, alice, charlie)
Membership Endpoints
Add the membership endpoints to your MemoryServiceProxyController:
// Membership management endpoints
@GetMapping("/{conversationId}/memberships")
public ResponseEntity<?> listConversationMemberships(@PathVariable String conversationId) {
return proxy.listConversationMemberships(conversationId, null, null);
}
@PostMapping("/{conversationId}/memberships")
public ResponseEntity<?> shareConversation(
@PathVariable String conversationId, @RequestBody String body) {
return proxy.shareConversation(conversationId, body);
}
@PatchMapping("/{conversationId}/memberships/{userId}")
public ResponseEntity<?> updateConversationMembership(
@PathVariable String conversationId,
@PathVariable String userId,
@RequestBody String body) {
return proxy.updateConversationMembership(conversationId, userId, body);
}
@DeleteMapping("/{conversationId}/memberships/{userId}")
public ResponseEntity<?> deleteConversationMembership(
@PathVariable String conversationId, @PathVariable String userId) {
return proxy.deleteConversationMembership(conversationId, userId); Why: These endpoints give the agent app’s frontend everything it needs to build a sharing UI — listing who has access, inviting new collaborators, promoting or demoting their access level, and revoking access. The MemoryServiceProxy handles authentication forwarding, so the Memory Service’s access-level enforcement (owner, manager, writer, reader) is automatically applied based on the authenticated user’s token.
Ownership Transfer Endpoints
Create a new OwnershipTransfersController:
package com.example.demo;
import io.github.chirino.memoryservice.client.MemoryServiceProxy;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/v1/ownership-transfers")
class OwnershipTransfersController {
private final MemoryServiceProxy proxy;
OwnershipTransfersController(MemoryServiceProxy proxy) {
this.proxy = proxy;
}
@GetMapping
public ResponseEntity<?> listPendingTransfers(
@RequestParam(value = "role", required = false) String role) {
return proxy.listPendingTransfers(role, null, null);
}
@PostMapping
public ResponseEntity<?> createOwnershipTransfer(@RequestBody String body) {
return proxy.createOwnershipTransfer(body);
}
@GetMapping("/{transferId}")
public ResponseEntity<?> getTransfer(@PathVariable String transferId) {
return proxy.getTransfer(transferId);
}
@PostMapping("/{transferId}/accept")
public ResponseEntity<?> acceptTransfer(@PathVariable String transferId) {
return proxy.acceptTransfer(transferId);
}
@DeleteMapping("/{transferId}")
public ResponseEntity<?> deleteTransfer(@PathVariable String transferId) {
return proxy.deleteTransfer(transferId);
}
} Why: Ownership transfers are a two-step flow — the current owner initiates, and the intended new owner must accept — which prevents accidental or unauthorized ownership changes. Separating this into its own controller keeps the membership CRUD in MemoryServiceProxyController clean and groups transfer lifecycle operations together for clarity.
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/381ca8a4-7ade-4f45-a9dd-6f5d71a30292 \
-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/381ca8a4-7ade-4f45-a9dd-6f5d71a30292/memberships \
-H "Authorization: Bearer $(get-token bob bob)" | jq Example output:
{
"data": [
{
"conversationId": "381ca8a4-7ade-4f45-a9dd-6f5d71a30292",
"userId": "bob",
"accessLevel": "owner",
"createdAt": "2026-03-06T14:59:41.214058Z"
}
],
"afterCursor": null
} Add a Member
# Share conversation with alice as a writer
curl -sSfX POST http://localhost:9090/v1/conversations/381ca8a4-7ade-4f45-a9dd-6f5d71a30292/memberships \
-H "Authorization: Bearer $(get-token bob bob)" \
-H "Content-Type: application/json" \
-d '{
"userId": "alice",
"accessLevel": "writer"
}' | jq Example output:
{
"conversationId": "381ca8a4-7ade-4f45-a9dd-6f5d71a30292",
"userId": "alice",
"accessLevel": "writer",
"createdAt": "2026-03-06T14:59:41.462683Z"
} Alice can now read and write to the conversation:
curl -sSfX POST http://localhost:9090/chat/381ca8a4-7ade-4f45-a9dd-6f5d71a30292 \
-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/381ca8a4-7ade-4f45-a9dd-6f5d71a30292/memberships/alice \
-H "Authorization: Bearer $(get-token bob bob)" \
-H "Content-Type: application/json" \
-d '{
"accessLevel": "manager"
}' | jq
Example response:
{
"conversationId": "381ca8a4-7ade-4f45-a9dd-6f5d71a30292",
"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/381ca8a4-7ade-4f45-a9dd-6f5d71a30292/memberships \
-H "Authorization: Bearer $(get-token alice alice)" \
-H "Content-Type: application/json" \
-d '{
"userId": "charlie",
"accessLevel": "reader"
}' | jq
Example response:
{
"conversationId": "381ca8a4-7ade-4f45-a9dd-6f5d71a30292",
"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/381ca8a4-7ade-4f45-a9dd-6f5d71a30292/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/381ca8a4-7ade-4f45-a9dd-6f5d71a30292 \
-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": "381ca8a4-7ade-4f45-a9dd-6f5d71a30292",
"newOwnerUserId": "alice"
}' | jq
Example response:
{
"id": "34c87e19-d146-46f0-a93a-493d9670c399",
"conversationId": "381ca8a4-7ade-4f45-a9dd-6f5d71a30292",
"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": "34c87e19-d146-46f0-a93a-493d9670c399",
"conversationId": "381ca8a4-7ade-4f45-a9dd-6f5d71a30292",
"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="34c87e19-d146-46f0-a93a-493d9670c399"
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/381ca8a4-7ade-4f45-a9dd-6f5d71a30292/memberships \
-H "Authorization: Bearer $(get-token alice alice)" | jq
Example response:
{
"data": [
{
"conversationId": "381ca8a4-7ade-4f45-a9dd-6f5d71a30292",
"userId": "alice",
"accessLevel": "owner",
"createdAt": "2025-01-10T14:33:15Z"
},
{
"conversationId": "381ca8a4-7ade-4f45-a9dd-6f5d71a30292",
"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="34c87e19-d146-46f0-a93a-493d9670c399"
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/spring/examples/doc-checkpoints/06-sharing