Build multiplayer SaaS apps with Liveblocks
Add a Figma-like multiplayer editor experience, share & invite dialogs, a document browser, and more using Liveblocks. Steven Fabre will teach us how.
Links & Resources
- https://twitter.com/stevenfabre
- https://liveblocks.io/
- https://crdt.tech/#:~:text=A%20Conflict%2Dfree%20Replicated%20Data,computers%20(known%20as%20replicas).
- https://github.com/yjs/yjs
- https://www.figma.com/blog/realtime-editing-of-ordered-sequences/
- https://nextjs-starter-kit.liveblocks.app/whiteboard/cFm1cUPWwfMHYVWkv7emI
- https://lwj-liveblocks.vercel.app/
- https://developers.cloudflare.com/workers/configuration/durable-objects/
- https://liveblocks.io/docs/tools/devtools
- https://liveblocks.io/comments
- https://liveblocks.io/discord
- https://twitter.com/liveblocks
- https://www.learnwithjason.dev/schedule/
Full Transcript
Click to toggle the visibility of the transcript
Captions provided by White Coat Captioning (https://whitecoatcaptioning.com/). Communication Access Realtime Translation (CART) is provided in order to facilitate communication accessibility and may not be a totally verbatim record of the proceedings.
JASON: Hello, everyone, and welcome to another episode of Learn with Jason where I need to get Steven on the screen. Oh, boy. There he is. There's Steven. So today on the show, we have Steven Fabre. How you doing?
STEVEN: I'm good. Thanks for having me today.
JASON: Yeah, I'm super excited to have you on the show. I think we're going to have a lot of fun. We're doing something that if you told me I had to build it in 90 minutes, I would tell you it's impossible. Before we talk about that, let's talk a little bit about you. For folks not familiar with you and your work, do you want to give us a bit of a background on yourself?
STEVEN: Yeah, sure. So I'm Steven Fabre. Steven Fabre in French. I grew up in France, and I'm the co founder and CEO of Liveblocks. And Liveblocks is a company that helps developers and companies make their applications and products just like Google docs, Figma, notion, those kind of products. We try to help the APIs and the tools to make that easy for you all.
JASON: I realized I forgot my color effects. Can't do that without the back lighting. Come on. [ Laughter ] Yeah, so this is something that I think is it's a trend I've noticed is that, you know, originally, it felt like only Google docs did this where you had this idea of multiple people were in one document and you could see their cursors and you were kind of following them around. And that was incredible. And then, it it didn't seem like anybody else had the money, the technology to do it, right? And all of a sudden, Figma introduced it, and we started seeing it in a lot of SaaS apps and now, it's sort of feeling like it's becoming like, I feel like it's almost becoming stable stakes, you need the presence API, you need to have collaborative editing. But how it just sounds so hard. [ Laughter ] Right? Like, this idea of needing to know, not only like, OK, user auth in and of itself is a big challenge. And then, there's the auth challenge of, like, now we have multiple people in and we need to be able to determine whether or not they're logged into the same instance or the same, like, organization or something. So you have to show just putting their Avatars in, all right, there's already a challenge there I'm seeing. But then, there's this idea, OK, now we've got documents, though, and in a document, one of the major challenges is that, like, documents are not just like these monolithic things. Tons and tons of pieces. And for someone to be editing this one and me to be editing this one and not to be unmergeable mess. My whole brain starts to hurt when I think about doing this. And so, if somebody told me I needed to build this, I think I would just say no. [ Laughter ]
STEVEN: No, I'm not going to do it, no.
JASON: From your standpoint, this is clearly something you saw as as a trend or as I imagine you wouldn't have started a company around it. So what one, why did you decide to take this bet? And then, I have a follow up question?
STEVEN: Yeah, that's a good question. So, honestly, I have no idea how to start a company around this. We kind of stumbled upon this with my co founder Guillaume, based in Montreal. And we, both of us, we used to work at Envision, and personally, I worked in tools for probably 10 to 15 years at this point. And while we were at Invision, we were working on a product called Invision Studio. I don't know if you're familiar with it, but it was this design tool that worked on the desktop, desktop based application where you could do UI design, but you could also do cross typing and animation. And obviously, it kind of Figma came along and it was browser based, multiplayer, it was like super performant. And so they were starting to take a lot of market share and design industry was looking at Figma and starting to use Figma. So we had this project there to essentially convert from desktop based, file based application. You actually save files to save them locally. And we had a project to convert that to a browser based applications where everything would just, you know, working the browser, and then you have multiple people kind of editing the same file. And
JASON: Right. So you did get that project that I would have just said no to. They were like, yeah, go do that.
STEVEN: Full disclaimer, I was not an engineer on that team. I was heading up design for the cloud project.
JASON: OK.
STEVEN: And, yeah, turns out, we were like a team of like, I think, at the peak, we were like, 6, 7 people just on this cloud project. And it took about a year and a half to literally go from desktop application to something that worked in the browser and that was still a very, like the experience was not as performant as it needed to be. There was a little bit of delay between the live cursors and the data of the document. So it worked, but it wasn't like a perfect experience that you would expect in the tool like Figma or Google docs or some of the other tools.
JASON: Sure.
STEVEN: That's, basically, where I first got exposed to this problem. Again, I still had no idea we started company run this at that time. I was a little bit frustrated I took so long. And to me, it felt like, look, Google, you know, Google, with Google docs, you know, 10, 15 years ago, they, they were able to like, take a lot of market shares from Microsoft office, because it worked like, you know, it was collaborative, multiplayer. You didn't have to install an app to get it to work. So distribution a lot better. The user experience is a lot better. And they were able to pull that off 10, 15 years ago. And then, Figma, the basically proved you could do this by a high quality design tool used by professionals. To me it felt at that point, pretty much every SaaS product was going to go in that direction. So, yeah, it was frustrating that it was so hard to do. At the time, I understand a lot of people would say no to this idea like you would. And it ended up leaving Invision, and with my co founder, Guillaume, we started working on the video/presentation tool. And we wanted to make it multiplayer and collaborative. And eventually, basically months into that project, basically, felt like what we had just experienced at Invision, we were like, damn, once again, we're spending all of our time trying to figure out the realtime collaboration of things and not focus on the actual tools themselves. That's when it clicked. Let's start a company around this. All the APIs and infrastructure we built for this tool, let's try to productify it and make it so any developer could use it for their own product. So let's that's how we stumbled upon this. It wasn't the master plan. We're going to be doing this kind of stuff that happened organically.
JASON: So I have more questions about the, like, I want to get into actually building with this. But to sort of contextualize this for somebody who hasn't attempted to build one of these and is maybe looking at this like it's a great big mystery box of, like, I don't know where I would start. Why is this so hard? Like, what is happening under the hood that makes this challenge so daunting for companies and like, why has it been such a blocker for companies that it takes a year and a half to build?
STEVEN: There's a ton of complexity involved in it. I think, to me, every piece that makes collaborative product, on their own, they're pretty difficult. The infrastructure piece, making it all work like Web socket, web socket servers, they're pretty hard to scale. Handling reconnections, like automatic connections of web socket is pretty difficult. If your computers go to sleep, you need to build a ping pong mechanism to reconnect. There's a lot of, like, stuff to figure out there. But I think, another thing that's really difficult, also, is to enable, as you mentioned earlier, multiple people to edit the same data at the same time. And resolve the conflicts in realtime and make sure that everybody sees the same state of an app.
JASON: Right.
STEVEN: Being able to work offline is one of the things, as well. And then, I would say that, what makes this even harder is to get all of the things to work very nicely together. Presence work scales and works nicely. Enabling multiple people to edit the document. Comments. How do you make sure the comment experience works nicely with the live document? All of those pieces making them tied together to create a cohesive experience. I think that's what makes it very hard. So yeah.
JASON: Right. Yeah. I think it's one of those things where, you know, what makes complexity so challenging is that individually any one of these things is relatively straightforward. Right? To set up a web socket connection is, you know, it's a web standard. You just say, like, give me a new web socket, upgrade my connection, but then, you are you start building state off of that. And then, if your connection gets lost, you have to maintain the state and make sure that you reestablish the connection and don't, like, you know, reset back to zero or lose all of somebody's progress because of a blip in the connection. And as you said, offline support. OK, if I go offline and get on an airplane, I'm going to go for 2 hours of just writing on this document. And when I come back online, that has to merge in with all of the changes that got made while I was working offline. Each one of these things is sort of mechanically simple. When you start thinking about the, you know, the 15, 20 different simple concepts that all have to fit together seamlessly now, it becomes incredibly complex and incredibly challenging to work on. Which is, you know, which is why, I think, if a company told me to do that, I was like, you know, it sounds like Invision was willing to put the resources on it. We'll give you a team, that team is only going to work on this. A lot of times, what happens, companies don't quite grasp the sneaky complexity of something they're asking for and go a couple of engineers can do that in a few weeks, right? It's like, uh, no. [ Laughter ]
STEVEN: It's a lot harder than you think sometimes, you put your head to it. It's just, yeah, never ending. Hopefully, hopefully, a little bit easier now.
JASON: Let's talk specifically about Liveblocks. Liveblocks is a company dedicated to sort of taming this complexity and making something you can drop into one of your existing SaaS product. So, like, it might be easier just to show, so tell me if it's easier to switch over and start coding. But like, as a developer, when I reach for Liveblocks, how am I fitting this in? Because of a lot of the stuff seems very idiosyncratic. The app needs the things it needs and doesn't need the things it doesn't need. How do you make something as complex as like, what Liveblocks is offering generic enough to fit into any SaaS application?
STEVEN: That's a good question. I think it depends. I mean, all of our customers have different use cases. So, it really depends on like what you're trying to build. And when you build, if you have an existing product, it's not like your product is collaborative or nothing, it's like, it's, you know, there's an incremental steps to get to like a Figma like or Google docs like experience. You don't need to have all of the things to make your app collaborative. Some people depending on the use case, if they have an existing product and they just want to make their product a little bit more realtime, maybe you can still rely on your own database and use some web socket broadcast events to make it a little bit more dynamic and automatically kind of refresh the page. And so, sometimes, you know, that's enough. Some customers want to add, like we're working on a new product around comments, at the moment. And, you know, some people want to add the comment panel. And that's fine. So we try to sort of package our products based on very specific part of the collaborative experience and the use cases, as well, that you're trying to build. So that's how we think about it. But it's not always easy. We try to abstract it in a way that depending on why you want to use, you can sort of choose and pick what you need.
JASON: Gotcha, OK. All right. I think any other questions I have, it's definitely going to be easier to show rather than tell. So why don't I switch us over into the paired programming view?
STEVEN: Let's do it.
JASON: I'm going to do it by moving this over here. And this over here. And clicking this button. OK. So this episode like every other episode is being live captioned. We've got P Diane here, thank you very much, Diane. And that is made possible through the support of our sponsors. We've got Netlify, kicking in to make the show more accessible to more people. And I do have more room for more sponsors if your company is feeling such a thing. We are talking to Steven, and if you are on Twitter, because it's always Twitter you can go give a follow there. [ Laughter ] Never, never! [ Laughter ] We're talking about Liveblocks today. So this is the this is the Liveblocks home page. From here, if I wanted to get started. Where should I go first? What should be my first step as a Liveblocks newb?
STEVEN: I think it's it really depends, but there is this multiple ways. Since we have quite a bit of time, it's probably, let's go through the docs and we have an interactive tutorial that takes you step by step through the concepts. That's probably the best way to get started and learn Liveblocks. And then, at the end, if we have time, I think I would love to show you the starter kit, which combines all of the products and concepts within Liveblocks, all the APIs and everything into one cohesive Next.js starter kit that includes all of the things for a Figma like application. Let's go into the tutorial here to get started and that's probably going to be the most relevant to you all.
JASON: OK. I'm in the tutorial.
STEVEN: Yes, Chris from our team built this. We shipped this about a month, month and a half ago. And it is to take you through the concepts of Liveblocks in an interactive way. And so, on the left here explain to you what Liveblocks is. And this is, essentially, the dynamic sort of interactive tutorial that you can use to code, you don't even need to open VSCode, just turn on the concepts. So here, all right. Get ready.
JASON: I'm ready. This is cool. OK. All right.
STEVEN: I think you need to hit return, perhaps, for it to 7:00.
JASON: Yeah, it updated to let's start, which is great.
STEVEN: OK. Nice. And if not, I'm stuck, show solution in the bottom left corner.
JASON: Yeah, yeah, yeah. This is similar to like the the Svelt docs do this, the Solid docs do this. You have to get to have the first experience by getting a problem and solve it and you've got the bailout button, or if you've done it before, you can say give me the code.
STEVEN: Exactly. We took a lot of inspiration there Svelt interactive tutorial. I think there's a few others, I think Astro has one, as well, maybe some others. Yeah, it's really hard when you build a DevTools product. Because there's different kinds of developers, like some of them want to go straight to the API reference and that's all they need. Some people want to learn through a simple tutorial, some people want to learn through examples. We tried to give different options based on what you prefer. But yeah, essentially, for Liveblocks. Everything starts with like obviously need to connect to Liveblocks. So here, we have this config file. Every time you create a project, the there's this Liveblocks config file you need to create. We have a comment to generate that NPX create Liveblocks app, which you have on the left here. And that generates this file for you automatically here. Putting your API key, which there's demo API key here. And then, we created a cofactory, if you scroll down on the right. That will give you give you basically all of the hooks you need within React to build collaborative experiences. There are hooks here to store the document data. Multiple people can edit that. And this is where you do that setup.
JASON: OK.
STEVEN: Here to continue. So you undo, redo, use mutation. It's a little bit blurred on my screen, by the way, it's hard to read all of the text, Jason.
JASON: I'll make it a little bit bigger herer so hopefully, that's a little bit easier to see.
STEVEN: Room provider, I think it should take you to the next step. Cool, you're connected.
JASON: Nice. Nice. Now we've got live cursors.
STEVEN: There's a little bit of magic happening. We'll get into how to build this in a little bit. But here, if you go to the next page. If you go on the at the top, as well. Cool, now that's they both work. So, here the idea is if you look on the left, is to basically, change the code so that in the app, TSX file, you actually connect to a room. The way we enable this within React application in Liveblocks, we have this room provider, and then, you just pass a room idea. And in most cases, if you were to build a Figma like product, the room would be the ID of the document. If you build a Google docs, idea of the document. Typically, that maps to a document. And the room is essentially the concept we use for, essentially, the digital space in which you collaborate together.
JASON: All right. And I have a room.
STEVEN: Yep, just pass the room. And
JASON: A room ID should be a string.
STEVEN: Yep.
JASON: A specific room ID?
STEVEN: You can use one, that's fine. And if you want, we if you scroll up a little bit on the left side, this is kind of up to you, but if you want to show a loading screen, you can use suspense. And you have a client side suspense, which is very similar to the one they use in React. So as the room is loading, you can have a loading state, which is pretty standard for most collaborative apps. Yeah. And then
JASON: Cool. I'll grab this and move it.
STEVEN: You don't always have to do this one, but I think it's a nice one to do.
JASON: This needs to be
STEVEN: Yeah, yep. We're doing pretty well, so far. It's pretty hard to do some live coding. Glad it works.
JASON: We're humming along.
STEVEN: Yes. All right. Concept. So I think the one thing that's really key this time with Liveblocks, I talked about the room concept. So the collaboration happens within the room. Within that, there's different things you can do. We have the concept of presence. Presence, you can use that for live cursors, show live, people currently in the room. The concept of broadcast, you know, where you can broadcast events, events to all of the people that are currently in the room. So if you want to say I'm Jason of here, and you broadcast that to everybody else. And then, we have storage. And storage is essentially persisted data store within the room. If you wanted to display others. Let's do I think live cursors using presence page here, we can jump to that. Going to be cool for people to see. So here, there is you see, we're updating the cursor. If you scroll down in the code, pretty straightforward, we do onpoint or move. And then, we handle the pointer move. And if you look at the function for pointer move. We take the events, client X, client Y, the coordinates of the cursor. And we say, update my presence, and we patch that in. And that's essentially how you will build live cursors. Now, if you wanted to display the cursors, you would need to here we're displaying the value. But we want to actually display a cursor component.
JASON: Got it.
STEVEN: Exactly. We have a hook, which is called use others. There you go. And that essentially lists all of the people that are currently live in the room and their presence. And so, if you scroll down, all you have to do within the div here is to pass like a cursor, I think you have a cursor imported at the top components. So you just, you know, cursor, X and Y
JASON: So this would be
STEVEN: This would be actually, you need to do a map.
JASON: Over this?
STEVEN: I think so.
JASON: Let's see. We'll go others.map and
STEVEN: Pass the cursor.
JASON: Each one of those will have a cursor. It would be presence, right?
STEVEN: I think it's going to be I forgot I think it's that's right, yep. I think that's right.
JASON: Presence and then, we want to return a cursor. And the cursor has an X, which looks like it would be presence.cursor.x. And Y would be the same. Presence.cursor.y.
STEVEN: Yes. Let's scroll down on the left, perhaps it's going to, I think we might be on the left left hand side, it should give you the oh, yes. OK. We used the connection ID to pass the key.
JASON: Got it. Got it. So first, we have to filter.
STEVEN: You have to filter because, yeah, the presence cursor value could be null.
JASON: Other presence cursor does not equal null.
STEVEN: Yes.
JASON: OK, and we'll build a little code here. And then, we get our map
STEVEN: You need to pass the connection ID as well as the key.
JASON: I need the connection ID. And then, we'll set a key I think I'm missing a
STEVEN: Something. I can't really see.
JASON: Yeah.
STEVEN: This looks good, though.
JASON: One.
STEVEN: Yes. Let's move your cursor, now. Let's see if that oh, there you go. As you can see, on the right hand side, you're moving and that's displaying a cursor component, which if you click on the cursor, that's essentially just an SVG, if you look at the file, that's probably just an SVG that looks like a cursor.
JASON: Mm hmm.
STEVEN: This is to show you the kind of stuff you can do with this. Obviously with presence, you can connect user information, we have a way to do a proper authentication endpoint, you can assign the name, the Avatar, and build all kind of stuff with this. All right. And perhaps, maybe before we get into the starter kit, maybe show some storage concepts, I think, just because I think that's probably the yeah. So storage, so we have what we call Liveblocks storage. And as I said earlier, this is essentially persisted data store where you would store documents and enable multiple people to edit that data simultaneously. And so, within storage, we have three different data types. Live object, very similar to a JavaScript object. A live map and a live list. But we make sure that when people edit items within storage, we resolve the conflicts and make sure everybody sees the same stuff. A good example of that, you know, if you were to build an ordered list of items
JASON: Mm hmm.
STEVEN: Say you do that locally, like a standard array.
JASON: Right.
STEVEN: That's easy because you know the index.
JASON: Right.
STEVEN: What if you have 5 people reordering things. How do you know the index? You need to resolve that stuff. And so, that's basically the kind of stuff Liveblocks handles for you.
JASON: Gotcha, OK. That's good because I think that's what like, those types of challenges are what lead to, like, I know I've been in some apps where if somebody's in a document, it'll show you that they're there, and so you get the presence, but then, the document locks when somebody else is editing. Right? And then, that person has to remember when they're done to leave the document or else nobody else can edit it, right? And so, those challenges are really, really hard to resolve. So this is this is exciting if this is one of those just works kind of moments.
STEVEN: Yeah. Exactly. And
JASON: First thing
STEVEN: Yeah. Same thing, we have this config file. So here, yeah, let's create a person. It's just a live object of the name. And the age.
JASON: OK. I've got my live object set up here.
STEVEN: One thing that's nice, also, with this config file, we try to do everything to optimize for typescript. Once you define your types here, everywhere in your app, as you type within VSCode, it'll tell you, like, the types probably, so you don't have to redefine them in multiple spots.
JASON: Right.
STEVEN: Pretty neat for the developer experience.
JASON: That is always wonderful. So I'm adding my initial storage, which is a person. That's going to be a new live object. And that live object, if I can avoid typos.
STEVEN: There you go.
JASON: We've added an object, right.
STEVEN: This is a way to set the initial document more or less, right? To display the storage, go back here. We have hooks to get the storage and, in that case, we want to get the person directly
JASON: And the way this works from what I can understand, is let's see, I need to actually import this. Import use storage from
STEVEN: Config.
JASON: Liveblocks config.
STEVEN: Mm hmm.
JASON: So then we've got we're calling this use storage. And that gives us the root. And the root is whatever gets put in here. Correct?
STEVEN: Yep.
JASON: So when I go to root.person, it is whatever the value is in .person.
STEVEN: Exactly. Your persisted state. Shared between everybody who has access to that room and persists over time. It's pretty much a database at this point. Persisted database.
JASON: It's persisted where? In Liveblocks?
STEVEN: Yeah, so, the storage is essentially every room every room is essentially a mini server that lives on the edge. And so, each room is close to whoever connects to that room first, it's going to take a close edge location, you'll be able to have really fast performance. So, in that case, Jason, if you were to start the room, I think you're in on the west coast, it would probably be location close to you. If I start it, I'm in Paris right now, I would be at a location in Paris. That's essentially what happens. So storage, it is, we have a database sort of attached to that room. That persists over time. And this is where you store your documents data.
JASON: Got it. So how so this is persistent in the sense that for as long as someone uses that room ID, this data is, is stable. It's not like if everybody disconnects and then we come back to it in a year, the data will still be here?
STEVEN: It will still be there. And as a developer, you have APIs if you want to clear the data, you have APIs to do this. If you want to, you know, sync it to your own TV, you can do that. Or if you want to do that as your own, you can do that, as well. By default, everything persists.
JASON: OK. And we've got a question from the chat on conflict resolution, which it sounds like we're just starting to edge up against this where we have, like, we've got storage now, right? How does this work when I start making changes to this?
STEVEN: Yeah, so it depends on the data type that you use. As I mentioned, we have, you know, live object, which is the one we're using here for person. We have live maps and live lists. So for live object, every key of that object, you can have, like the name, the name Mary, I could be editing age at the same time and we'll make sure this works. So, this is we've built our own sort of logic and some of the conflicts get resolved in the backend, but some of them depends on the order of actions. So those it's inspired by but it's not a pure I'm not sure if people are familiar with this.
JASON: Yeah, a CRDT is a
STEVEN: Conflict free replicated datatypes which is you know, I'm not like an engineer by trade, so this is way above the kind of stuff I can do. But there's a ton of like paperwork that's been done on this. There's a ton of cool open source projects, like YJS is one of them. That's kind of the big one right now that's been adopted. By Kevin Janes. That's typically what a CRDT is. Gives you different datatypes so that multiple people can manipulate that data together and kind of resolve the conflicts.
JASON: Mm hmm.
STEVEN: That's essentially, we take a very similar approach. And given background, Guillaume and myself, working at Invision and being super focused on creative tools, we actually took a lot of inspiration from what Figma was doing. We spent a lot of time trying to reverse engineer how they did things, like what happens when multiple people reorder items within the list. Which happens fairly often. You have list of layers and you can sort of move them around.
JASON: Right.
STEVEN: That's the kind of stuff we've been thinking about. For live list, maybe I'm too deep into here, but happy to share some blog posts from Figma, but Evan wrote a great blog post about how they reorder lists at Figma. And they use something called fractional indexing. And it's pretty cool. Essentially, instead of using like a standard index within lists yeah, this is it, I think. This tells you a little bit more about it. Essentially when you reorder some things to enable multiple people to reorder that list, essentially, when you add an item between an index and another one, you basically make the division between the two and give a fractional index. This always remains in the right order. So if you have something on index zero point. If you want to add something between 0.2 and 0.3. It's going to create 0.25 index. And it's always going to be in the same order no matter what people do. For everybody that kind of changes the order of things within that list. That's the technique we use for this.
JASON: Very cool.
STEVEN: Yep.
JASON: OK. So we I've moved to the modifying storage part. Do you want to let's see, we have another 45 minutes or so that we can code. Do you want to keep going through this or go to the template?
STEVEN: You know what, I think probably good enough kind of concepts. I think, yeah, let's get into the template so we can get into VSCode and actually do more coding.
JASON: And the template is
STEVEN: I think you Google Vercel Liveblocks, you should be able to find it.
JASON: My other window here this one here, right?
STEVEN: Yeah, maybe we can give a glimpse of what this looks like. There's a view demo.
JASON: Here it is.
STEVEN: Yeah. You're logged in, but if you log out, starter kit, Next.js template starter kit that includes, essentially, your marketing page where you are logged out of your application. Includes authentication to sign in and authentication through next auth and authenticate with the provider zero or whatever. And then, if you sign in, just to show people the kind of stuff you can do with this. Here, we picked a fake sign in for now. But you can sign in as somebody here. Here you have a work space that you have in every SaaS collaborative product. You have your draft, you have groups. And here you can create a new draft, for instance. Whiteboard is the only one available now. And this is your whiteboard, it's a basic whiteboard. It's meant to help you get started. But we handle all of the routing and stuff S. The ID of the room. This is what we used to do, the room provider, concept of presence. If you open this side by side, Jason, people will see this is all multiplayer and collaboration.
JASON: Grab the link here and do a split window.
STEVEN: So there you go. This is basically you're logged into this and you see, basically, can add different notes. And you have the live cursors and that sort of stuff.
JASON: Yeah. I believe if I throw this in here, anybody who wants to. Here's the thing, chat, if y'all come in here and start doing inappropriate stuff, we're going to be fighting. But, yeah, so this is this is cool stuff, though. It makes it possible to pretty quickly get in and start building this. So, this is is this like whiteboard part the drag and drop, is that built into Liveblocks? Or is this is this kind of like a demo app built on top of Liveblocks?
STEVEN: This is more like a demo app built on top of Liveblocks.
JASON: Right.
STEVEN: As I showed in the sorry, the interactive tutorial we went through.
JASON: Mm hmm.
STEVEN: We only provide the kind of hooks and APIs to enable you to build pretty much I any experiences.
JASON: Right.
STEVEN: We're starting to invest more into actually providing actual React components that include the prebuilt features. What I was thinking of today is kind of show you how that works and how we can start building some of those kind of collaborative experiences.
JASON: Yeah, very cool. All right. I'm ready. Let's do it.
STEVEN: Let's do it. If you go to Liveblocks.io/starterkit. I think you have it open maybe somewhere, or maybe not.
JASON: This one?
STEVEN: No, I want to get the NPX, because I don't remember it by heart. Yeah. Starter kit, I think there's a dash maybe, I forgot, maybe not. There you go. You want to copy this. And there you go. You can paste it. Wherever.
JASON: I'm going to run this here.
STEVEN: Yep.
JASON: And this is going to
STEVEN: Take you through the steps.
JASON: We'll call this LWJ Liveblocks.
STEVEN: I recommend picking demo for now, it's going to be harder to do
JASON: Looksing like deployment built in, we'll hit yes. Do I want to install? Yes. Open browser to continue setup, sure.
STEVEN: There you go, you are in Vercel now. You're not logged in. You need to probably log in with Vercel. There you go.
JASON: OK. We're going to do this.
STEVEN: Yep.
JASON: I can't do, sorry, Vercel won't let you do organization. So I'll use my personal account. They want that money for those organizations.
STEVEN: There you go. So now it's asking you to at the Liveblocks integration.
JASON: OK.
STEVEN: This should open. A little Liveblocks window. There you go separate the SKT from Liveblocks.
JASON: And we're going, we're back.
STEVEN: You can pick, create a new project or two kind of projects.
JASON: We'll go with the dev project. Seems like the right call.
STEVEN: Pick import, API key.
JASON: Nice. And that sent it to Vercel?
STEVEN: Yeah, it'll deploy automatically. If you wait, takes probably 30 seconds, usually, within Vercel.
JASON: OK. Repo.
STEVEN: And what's nice, this is automatically linked to your GitHub, as well.
JASON: Nice. Whatever you set up in Vercel, that's linked.
JASON: Nice. This is like, cool, yeah.
STEVEN: This is a way to quickly get started, you know, if you use Next, and if you want to play around with Liveblocks and see the stuff you want to build, it's great. But if you build a completely new component from scratch, you get the work space view, the marketing site view logged out. When you don't have access to a document, like, this permissions involved in this, you know, if you view permissions, write permissions, everything in there is included. All that stuff.
JASON: Nice. Still building, looks like, on the left.
STEVEN: Look at the animation, now, from Vercel.
JASON: So
STEVEN: You have your URL, I think, if you go here. Yeah, this is live. Like
JASON: OK. We've got it. Question about your database. What's the default database for Liveblocks? You mentioned it was an edge storage?
STEVEN: Yeah, behind the scenes, we rely on cloudfare workers. It's an awesome infrastructure. And essentially, everything we do at Liveblocks. Not everything, but a good chunk relies on that infrastructure. Each storage data store I mentioned, this is stored on I forgot the name, cloudfare workers. Durable objects. I don't know if you're familiar.
JASON: Durable objects, yeah, yeah.
STEVEN: Each room lives on an edge location, and each edge location has durable object in which we store Liveblocks storage. And that's kind of the persisted system we use for storing data.
JASON: Cool. Very cool. Let's see. So this is the demo we just looked at. This is all going to look the same as it did before.
STEVEN: Exactly the same.
JASON: Let's dig into what the code does.
STEVEN: Yeah, instead of floating the hosted version, you probably want to run npm run dev to work locally. I don't know if you need to install. I think it does install automatically.
JASON: Yeah, the install was part of the CLI.
STEVEN: Nice. Big shout out to Chris and the team who built this awesome kind of experience with the CLI.
JASON: Yeah, this is slick. Oh, it's already running. I was waiting for it to do something. We've got local host 3000. And here is our app. This is the local version of the app?
STEVEN: Little disclaimer, we don't use the app folder on this. This is the old kind of setup.
JASON: It seems like I don't know, seems like app folder is a little early to make the default. Seems like they're still kind of launching things. This almost works now.
STEVEN: We're starting to move a lot of examples to the app folder. Kind of the starter kit is on this kind of old way. I'm sure a lot of people are familiar with the Next.js, where they do the routing. What could be cool is to dive into the whiteboard experience.
JASON: Yeah, let's do it.
STEVEN: Here, you pass the ID. This is essentially all you got the kind of room ID when you create a new whiteboard.
JASON: Mm hmm.
STEVEN: We should be loading the whiteboard component in this, I believe.
JASON: We've got the whiteboard document view. Scroll down a bit. Props. Is it the whiteboard component?
STEVEN: Yeah, this is it. One thing that could be cool. Let's think about what we could build here. Perhaps, I thought something that could be interesting is we could make it so those notes on the whiteboard, perhaps, we could have some kind of resolved check box but I think this is show the concepts to people, perhaps. So here we got
JASON: We've got our notes. Let me see if I can let's see let's do a quick intuition test. We walked through how storage works. And we walked through how to access that. And so, let's see if I can figure out how to make this work. I'm going to look for where we're setting up our room provider. And my guess is let's see, my instinct is to start looking out here. There's a session provider. We've got our index. Not there room provider coming in. Room provider. We've got our room provider. And our room provider should be getting initial storage. I want to find where that is set. Initial storage is a new live map. In it works like a JavaScript map object, we can set any key we want.
STEVEN: Yes.
JASON: OK. That's fine.
STEVEN: If you want to see the state of your like, everything, if you want to see the state typescript point of view how the type is set up, usually, just look for Liveblocks.config, that's why we did that setup. Scroll down a little bit. This is a little bit more complicated than usual. But here, there it is. You see your storage is like a live map of notes. And then, a note is
JASON: Let's go
STEVEN: Yeah.
JASON: Right? So we can set up a resolved, like, toggle.
STEVEN: Yep. And that's pretty much all you need to do here in terms of setting up the data structure for it. But now, we need to include it here right where we set up the note. You want to set up a nice default. So that's good.
JASON: By default, it is not resolved. OK.
STEVEN: And
JASON: Down where we render the notes, handle no change. Let's get it to display first. Here's our whiteboard note. And that is defined where? That is here. Let's get this open.
STEVEN: Yep.
JASON: Let me get our note.
STEVEN: Yep.
JASON: We should be able to set something like, we've got a div.
STEVEN: Here, I believe the button that's the close button the X icon. I think we actually have a check box component. Maybe that might make it a bit easier for you.
JASON: This one?
STEVEN: I think we use it somewhere else. That makes it easy for you to adhere. Yeah, checked.
JASON: And we would be able to say is it just notes? Resolved. Hey, autocomplete already a victory. Do we need anything else?
STEVEN: I believe you need I haven't check third degree component, in show value, and make sure you have a handler for an on value change, to actually change the storage data for that.
JASON: OK. So this is the part I actually have to define. But so, if we leave this off to start, then what we should be able to see is at least a reporting of what's happening.
STEVEN: Yes.
JASON: If I go in here, and choose a profile, and we'll open up a new whiteboard.
STEVEN: Mm hmm.
JASON: Add a note. And we have a check box.
STEVEN: Maybe you want to try to see if it works if you open it with multiple windows.
JASON: Yeah, let's do that.
STEVEN: That should be live.
JASON: So then to can I split? I can split. We'll add a right split. And we'll go to local host 3000, again.
STEVEN: And it's something I haven't showed you yet. Maybe I'm going two different actions here, but if you were to open like a incognito window, I don't know how that works with arc, but you could try to open the URL and you don't have access to it. This is all of the kind of flows that are handled, as well.
JASON: Mm hmm. Let me get this one open.
STEVEN: You see?
JASON: Not allowed access.
STEVEN: Exactly. You could sign in as somebody else, this person, for instance, and on the top, like, share if you could share dialog.
JASON: Share.
STEVEN: And you could add the other person. If you get the email, it was it's not going to show up automatically here.
JASON: There we go.
STEVEN: Add this person here. They have read access right now. If you open the other one or whatever.
JASON: OK. Open here. Now I can see it.
STEVEN: The real thing here is that the workspace updated automatically. You didn't have to refresh. As soon as you get access, as soon as you add people, all of that stuff is handled automatically.
JASON: OK.
STEVEN: You click this and be able to see it as another person.
JASON: Can I get this to I don't know why this one won't show up for me.
STEVEN: I think
JASON: Both at the same time. Now, if I go in here and I make edit, I get edit access immediately.
STEVEN: I don't think this is showing on your screen, Jason.
JASON: It's not?
STEVEN: I'm just seeing the code and on the left I'm seeing the two windows.
JASON: What's going on here? It like my desktop froze in that window.
STEVEN: I'm seeing the code on the right and the two arc windows.
JASON: Yeah, let me see if I can
JASON: I don't know why that happened, but it froze on me. OK so this is
[ Laughter ]
STEVEN: All good.
JASON: Now, it's working, we have what we want. We've got the shared cursor, and it did update live. So in this, I'm Mislav in this one and Onjolly on this one. If I turn off edit access, it updates right away.
STEVEN: And you can't add anything at that point. But you can see the cursor, if you add a note, you'll be able to see it. If you click resolve, you should be able to see it, as well. There you go.
JASON: Not yet because we haven't
STEVEN: You're right. You haven't set that up yet, that's right.
JASON: Now, to make this sync up, we have the check status. We have the initial value. We need to, then, add an on value change. And we need to make a function for this. So the function that I want to create is we'll just, we'll do a handle resolved, I guess. We'll say toggle. And then, we need to do something but for now, we can
STEVEN: Yeah.
JASON: And drop this in here. And handle resolve toggle. OK.
STEVEN: Yes.
JASON: Right now, it doesn't do anything, but we need to make it update our note storage.
STEVEN: So to help you here, we have a use mutation hook to mutate on the storage, but what will be easier for you, perhaps, is to go to the whiteboard component and look how we do like a delete, for instance, and that would give you a hint of, like, kind of how you need to set that up for the check box.
JASON: Change handle. Let's see, change is probably OK, right?
STEVEN: Delete would probably be easier. If you delete one, handle.
JASON: Delete, let's get to that.
STEVEN: Yeah, there you go.
JASON: Use the mutation. Mutation gets storage and then, is this my own user? And then, I get
STEVEN: This is your user, and we have a permission system we haven't go through this yet, you can actually, in that case, you know, if the user is has like permissions, you don't want that user to be able to edit. So this is kind of a check you can be potentially doing, as well. With the check and without the check and see what happens for the
JASON: Got it. I can take this one and I can drop this in here. And instead of deleting our note, first and foremost, I need to make sure we're actually importing this. And then, we want to
STEVEN: You want to actually get the ID of note. Right now, you're getting the notes. So you want to get the actually, just the ID, I believe if you have the ID. Into the notes as a prop. So go up, so you probably want to go up this ID.
JASON: OK.
STEVEN: And I think, yeah, and then, if you go back up a little bit, in the use mutation, at the top, this one I don't think it's node ID. What you're going to be getting here is is checked or checked, I guess. That's basically the value you're going to get from the call from the component.
JASON: OK. OK.
STEVEN: Yep. And so now, you should have your note. So what we want to do is actually set a specific property of that live object to whatever value we need. Set resolved, checked. And
JASON: What? Oh, it's a prettier problem. Oh
STEVEN: Yep. So I think this should work.
JASON: Theoretically speaking look at that! This is great. It works it does what I expect. Use mutation is straightforward. We get kind of a context object with Liveblocks data. And we get a piece of data from the this is the only piece that feels a little bit magic to me. How does the on value change? It just sends back?
STEVEN: I believe on value change sends the checked value. Yeah.
JASON: Handle change. OK. So it's you wrote an abstraction over how it works? I got it. I got it.
STEVEN: Yeah, this component, but essentially, you could build a standard kind of input.
JASON: Got it.
STEVEN: That's the value you get in the mutation there.
JASON: Very, very cool this just works. This is a it's a nice like it's a nice API. It feels familiar. I've seen this approach to handling data mutation in a bunch of apps. It doesn't feel like, like at no point here when I'm looking at this, am I thinking about, how do I sync this with all of these other people who are looking at this right now? I'm just thinking about how do I get data to update in my app? Well, I get the note by its ID and set the value to the new value. And everything else just sort of happens.
STEVEN: That's right. The idea, the approach we're taking is we want this to feel very familiar to, essentially, if you were to build an app locally with React, we want it to feel very similar to that. So that, you don't need to really, you know, think about, like, oh, my gosh, config resolution or what is going to happen on the server. We try to take away that complexity and make it super familiar to any React frontend developer so they can build the multiplayer experiences very easily. That's kind of idea of storage and APIs we have for React.
JASON: This is really nice. This feels good. And you know, for anybody who wasn't, wasn't paying attention when we set this up initially, we've got, I'm an incognito on this one. I'm two users. I am Mislav in this one and Anjoli in this one and this one is read only, I can't do anything. But when I come over here, I can edit, I can move. It is like all of that is happening and it's being reflected in realtime. And the code to make that happen is basically, a big stack of these mutations. [ Laughter ]
STEVEN: A big stack of mutations. There's a lot of code here, for sure. It's pretty straightforward. You do mutations to update the storage. And then, you have access to kind of list maps and objects that are all live.
JASON: Mm hmm.
STEVEN: And we handle all of that stuff for you. Jason, we haven't done this. I don't know if we have a lot of time. But maybe one thing that could be interesting to look at is multiplayer undo redo.
JASON: Yeah. Why not?
STEVEN: If you look at the window on the top, you get two little buttons to undo and redo. Maybe you can look at the code for this. We probably make it easier for people. So if you go maybe go to the whiteboard component. I'm not exactly sure where the toolbar is. There's got to be a toolbar.
JASON: Tool tip undo, tool tip redo. Here it is
STEVEN: Yeah. So we have a history. A use history hook. So if you click that, you should be able take you to that hook. There you go. And then, based on that, you can literally call history undo or history redo. And that will that will, basically, enable that.
JASON: So anytime we send any mutation that it's going into a stack, now is that and you said that's currently limited to let me give edit access here, again.
STEVEN: Yep.
JASON: I'm going to do is I'm going to say no one. And then, we say, no 2, and then, up here, I'm going to
STEVEN: Yeah. That's hard to do otherwise. We're trying to make that very easy. Yeah, you can map that. You know, here, we mapped that to buttons. But you can map it to keyboard shortcut. That sort of stuff. And call that thing. One thing that's maybe interesting to touch on related to this. If you there are cases where you actually don't want to include mutations in the undo/redo stack.
JASON: I thought of one. I noticed as I was undoing and redoing, I noticed the presence. The presence change was one of those things. And like, I don't really care about undoing whether or not, you know, this user was part of the presence stack. I want to skip that one and go to the next key press.
STEVEN: Correct. You can actually ignore that stuff if you want.
JASON: OK.
STEVEN: Especially, another use case maybe that can demo to you here. If you were to drag the like a node. When you mouse down, between when you mouse down and when you mouse up, if you undo, you don't want to have to do 20 undo to go back to the initial state. So this is one of those things, as well, where we I don't exactly know where that is. There's probably a function here that's called set where we set kind of the XY coordinates. Of the node handle.
JASON: Pointer down, history pause. Yeah, we're kind of stopping the history to not end up with a million events. OK.
STEVEN: Exactly. That's essentially a way to do it. And then, for presence, we actually have a setting when you set the presence to say, including history, yes or no, and depending on that, you can include it or not. It's kind of up to you to decide. So I think the presence happens when you select a whiteboard note, I think? Probably like a
JASON: Whiteboard note, and we've got
STEVEN: I think it's unfocused that's being called. It's probably going to be that event's going to be in whiteboard, I think. Because it's just imported from whiteboard. There you go. And if you click unfocused, I believe we're doing something there to set the presence. See this is
JASON: Mm hmm.
STEVEN: If you go to the config file, you will see the presence in this example is just not just it's not it's probably not just a cursor, it's also like what do you have selected? What you could do is, if you go back to the whiteboard handle not update. Oh, because it's stored on the OK, it stored on the actual note itself. So selected by this is something for presence to kind of decide, I don't want to include, you can decide not to have it in the undo stack, which is pretty handy.
JASON: Got it. Yeah. Theoretically, we could turn these particular ones off and it should why don't we try it?
STEVEN: Let's try it.
JASON: Oh, this gives us the ability to lock these, right? If it's selected by me, somebody else can't select it. That's not what it's doing right now, right?
STEVEN: What does handle note update do?
JASON: Handle note update. Handle note update.
STEVEN: Get note, note update. OK. Note ID.
JASON: It's throwing whatever we want in there.
STEVEN: Yeah. In that case, I think there's different ways to build the selection, like, typically, the way I like to do it, and I don't think this is how it's handled here is to actually store within presence, you could store the ID of a note and then actually use that information to show selection instead of doing something like this.
JASON: Mm hmm.
STEVEN: There are different ways based on what you want to do. But
JASON: You'd have to look at what you actually want to deal with. If I go back down here and we say we don't really care who is looking at the app right now. We save this, then, when I come in here
STEVEN: Nothing happens.
JASON: Yeah. Nothing happens. And then, make some changes here. Change there. Come back up here. Do some updates, undos. Right? So all kind of leads where we want to be. Where we've got the ability to do undo, redo, all of that good stuff. This is great. Like, I can see, you know, what I like about this, it sort of takes this very big complicated knot of thinking about not just storage, not just application state, and distribution and syncing of that state. Reduces to more granular controls. When someone clicks a thing, what do you want to happen? And then, it syncs across all the different people looking at the app, and we end up with this sort of it's a it's complex because a lot is happening but it's not complex because you have to juggle a lot of things. It's a complex stack, a very of simple interactions. And I think that's about the best you can hope for when you're getting into something multiplayer is that, you know, you're not thinking about how does this fit into the broader context of like all of the ways these apps plug together. But rather, how does the complex state of this app function in this app? Like, if I was going to write this just with React state, I'm going to put together my own custom context, I don't know it would be a lot different in terms of what I build in what's being built here. I think that's good. From a mental model standpoint, it's not adding yet another thing you've got to juggle. If you can build a React app, you can build a multiplayer React app. And that's the sort of big win here with these sorts of abstractions. It lets you use your skills as they are today. You don't have to add, like, yet another layer of expertise. [ Laughter ]
STEVEN: Yeah, that's pretty much the angle. I'm glad to hear that from you. We're trying to make it super familiar to, you know, React system. Like, the way you would build your React app as a single player would be almost identical. It would be pretty easy to write it single player and switch it multiplayer afterwards. Just like
JASON: Especially given that, you know, with stuff like use mutation, use query, these are all becoming common APIs. And so, I can see this feeling pretty familiar. I think, you know, where it gets tricky is when you've got the subtle differences. Like, you know, a storage API isn't going to be in the context API, right? So you have different ways of accessing things. But the general mental model is good. Like, it lets me work in a way that I already understand. And this is this looks to me the same as if I was using something that wasn't multiplayer. Like if I was using a MongoDB, it would feel pretty same. You've got your database object you're going to do, like get database, and you make a change. And then, you would need another piece if you wanted to do to the presence, whereas this one is all kind of rolled up in the thing. Yeah, I like this, this is really nice to work with. Does this have you're using durable objects. Do you have other integrations? If developers want to customize this stuff. Which pieces are, you've got to use Liveblocks exactly as is versus you can do whatever you want?
STEVEN: Yeah, right now, you sort of have to use Liveblocks as is. So we provide the different hooks and different ways to integrate. To integrate, sorry. Yeah, we are working on different ways to enable people to do more custom stuff. This is not quite that hasn't quite landed yet. Enable people to kind of level infrastructure to do custom stuff, as well. Right now, you kind of have to use exactly the APIs and that sort of stuff. We have one package for React, which is the one we've used here. But we also have packages for state management libraries like Redux and one for JavaScript, which is pretty cool. Even if you use Vue or Svelt, you can integrate directly with the client API. React API we use today is just an abstraction on top of that.
JASON: Got it. Cool. There's these packages and then, there's also the straight up REST API.
STEVEN: This is something we also provide with the platform, everything that you use today actually comes with like a lot of stuff you can do on the dashboard itself. We have REST APIs if you want to fetch a specific room or users within the room or the storage data of that room. You can do that. And also, stuff like web hooks. We have a good amount of customers using web hooks to sync data or do that sort of stuff.
JASON: Right. Yeah.
STEVEN: And, uh, maybe we should've done this at the beginning. But we also have DevTools browser extension. Which I think it's pretty handy. Like, you know, I remember in the early days of React when the DevTools extension came out. It just made building React applications so much easier. Trying to do something similar here. So we have if you ever want to build a multiplayer application, we have this kind of base tools you can install, which is pretty handy. Shows you all of the storage kind of data, presence, makes it very easy to build, like see your state in realtime because you have multiple people that can manipulate that state. It's pretty helpful to get a view of that.
JASON: Yeah, this is slick. So we get a view into the storage, a view into the presence, you get your history. So what we were just looking at with the presence events, we could see these in history and kind of take a look and see like, oh, we don't need that one. We can skip that kind of stuff.
STEVEN: Correct.
JASON: This is like a reimplementing Excel kind of thing. Cool, very, very cool. That's super handy. So for anybody who is going to be playing with that, go check out the DevTools. There's a question from Jacob in the chat. What kind of custom use cases are you expecting or looking to support in the near term? Which I guess is what's the roadmap? [ Laughter ]
STEVEN: What is the roadmap? Yeah. There's in terms of use cases, right now, everything we looked at today is the roadmap.
JASON: Mm hmm.
STEVEN: Tools and APIs to build multiplayer edit torrs. .
JASON: Right.
STEVEN: Creating an artifact, you can do that in presence and storage. We are working on comments, which is a different kind of use case that's not directly related to the multiplayer itself. But this is a collaboration use case to us is very important to be able to mention somebody in a document so they can, like, get notified and comment that document.
JASON: Mm hmm.
STEVEN: This is a big part of collaboration. Something we're enabling now. We are adding more people to the beta at the moment. If you're interested, feel free to sign up for that.
JASON: Cool.
STEVEN: In implementing custom codes, we want to enable people to kind of deploy their own sort of code into this infrastructure, as well. So it will be pretty flexible in terms of what you want to do. If you want to add custom logic or permissions, this is something we want to enable, as well.
JASON: This is the comments beta if you want to get in on that. Awesome. Well, so if folks want to go deeper, like we've got the website, we've got we've got this list of products, we've got the docs, DevTools, where else should folks go? What else do you think people should check out if they want to get started with Liveblocks?
STEVEN: Yeah, I mean, check out the website, the docs is good, quite active on Discord and this is pretty big community. Kind of helping each other out as they build multiplayer applications there. There should be a link somewhere in the footer here. And then, I guess, Twitter probably, @Liveblocks if you want to follow us. And, yeah, please let us know if you have questions, thoughts, feedback, feature IDs. I'd love to hear it. Discord
JASON: Awesome. Great. All right. Well, I think that is about all of our time for today. I'll take this as a great opportunity to wrap it up. If you want to hear more, you know, check out Liveblocks and all of the links we've shared or follow Steven. And this episode, like every episode has been live captioned. We've had Diane with us here all day, thank you so much, Diane, from White Coat Captioning, and that's made possible through Netlify and Vets Who Code. While you're checking out things on the site, make sure you look at the schedule, we've got stuff getting added all the time. Very, very excited about this one. We've got UnaKravitz coming on. New APIs and functionality. That's going to be an absolute blast. We are also looking at we're going to talk about Couchbase, I've got to get that up on the website. Excited about that. We're going to have Myriam Suzanne and doing a deep dive into container queries. If there's something you want to see, you can always get on the newsletter, send me a message or get on the Discord and do that, as well. With that, we're going to call this one done, Steven. Any parting words before we wrap this up?
STEVEN: No, I want to say thank you for giving me an opportunity to participate in the show. That was really fun. I hope people enjoyed it. And hopefully, people will try it out.
JASON: We'll find somebody to raid and see you all next time.
STEVEN: Thanks.
Learn With Jason is made possible by our sponsors: