To make an object freely transferable, use a combination of key and store abilities.

/// A freely transfererrable Wrapper for custom data.
module examples::wrapper {
    /// An object with `store` can be transferred in any
    /// module without a custom transfer implementation.
    public struct Wrapper<T: store> has key, store {
        id: UID,
        contents: T

    /// View function to read contents of a `Container`.
    public fun contents<T: store>(c: &Wrapper<T>): &T {

    /// Anyone can create a new object
    public fun create<T: store>(
        contents: T, ctx: &mut TxContext
    ): Wrapper<T> {
        Wrapper {
            id: object::new(ctx),

    /// Destroy `Wrapper` and get T.
    public fun destroy<T: store> (c: Wrapper<T>): T {
        let Wrapper { id, contents } = c;

module examples::profile {
    use sui::url::{Self, Url};
    use std::string::String;

    // using Wrapper functionality
    use examples::wrapper::{Self, Wrapper};

    /// Profile information, not an object, can be wrapped
    /// into a transferable container
    public struct ProfileInfo has store {
        name: String,
        url: Url

    /// Read `name` field from `ProfileInfo`.
    public fun name(info: &ProfileInfo): &String {

    /// Read `url` field from `ProfileInfo`.
    public fun url(info: &ProfileInfo): &Url {

    /// Creates new `ProfileInfo` and wraps into `Wrapper`.
    /// Then transfers to sender.
    public fun create_profile(
        name: vector<u8>, url: vector<u8>, ctx: &mut TxContext
    ): Wrapper<ProfileInfo> {
        // create a new container and wrap ProfileInfo into it
        // return the container to the caller
        wrapper::create(ProfileInfo {
            name: name.to_string(),
            url: url::new_unsafe_from_bytes(url)
        }, ctx)