After the Meltdown: rethinking event management platforms.

Stefan Adolf
Nov 5 · 21 min read

A decentralized approach for managing events, conferences and attendees

Beginning of October many local community organizers received a notification by wework owned meetup.com stating that they plan to evaluate changes to their business model. Put shortly, their idea was to either charge every attendee a 2 Dollar fee for every RSVP while at the same time lowering the yearly costs for event organizers to 24 Dollars. What effectively sounds like a worthwhile idea to raise attendance rates, it could easily be interpreted as a desperate move by wework to monetize their remaining assets. If you’re interested in the whole story, The Verge has got a fantastic writeup here.

This incident that I personally refer to as “meetup meltdown” makes one thing very clear: as long as you rely on a third party you will fully depend on their good will. They set the rules, that you have to play along and they can change them any time. Even worse: if they go down because of this, they’ll take you with them.

In the aftermath of the announcement many people jumped over board immediately. Regardless of the not too evil idea to charge (and even return!) a ridiculously low fee, nobody wanted to be the one to pay the bill for wework’s disastrous attempt to go public in September. Which is a legit decision. But here’s the issue: if you’re not on meetup.com — where are you building your community then? Suddenly alternatives were popping up like flowers in the desert after a rain shower: it felt like thousands of tweets by anxious meetup hosts hit our timelines, asking “Which alternative would you recommend?And the world is not short of them.

If you think about it for a moment, any attempt to switch from meetup.com to yet another platform will end in an evil circle of doom: once that platform has to handle the whole load of planet earth’s meetup movements, it will sooner or later also come to the conclusion that charging a fee might be a good idea for one or another reason. So it’s time for smarter approaches that allow them to think slightly different.

FreeCodeCamp founder Quincy Larson posted a tweet only hours after the meltdown incident encouraging his worldwide active coder community to start building a distributed alternative for FCC chapters right now. And the community followed.

Within hours a dedicated Discord channel went wild with around one thousand more or less experienced hackers, many of them seemingly skilled in frontend matters. Only a day later their github repo opened up and the discussion was piped into an endless stream of feature requests. Everybody seemed to have a good idea of how the organization of local community events could be improved. Everybody wanted to be aboard when the whole world starts to establish an open source meetup community platform. The first thing to see on the README was a relational diagram depicting the relations between users, organizers, venues and events — and it was no coincidence that it looked similar to the concepts that we knew so far as: meetup.com.

The FreeCodeCamp “chapter” repo takes it one step further, though. They absolutely recognized that a centralized platform will sooner or later end up with similar issues as our federated friends, so they want to distribute the moving parts on as many shoulders as possible. Local communities would run their own nodes and install the containerized chapter software on them.

The only centralized part in the system — as the time of writing — would be a discovery service making it possible for new users to find their local community’s hub nodes. A splendid idea. But you’d still need encouraged locals who are willing to take action and install and maintain a running chapter node (which obviously doesn’t come for free) and tell everybody to create accounts on them.

This kind of distributed linked network is not brand new: Mastodon and Matrix (the backbone of Riot.im) are quite successfully building Twitter and Telegram replacements. Kazaa and Diaspora were showing early that P2P controlled filesystems and social networks, operated by globally distributed peer nodes had the perspective to break the restrictions of centralization.

But do you really want to build software using relational databases and machine based application hosting in the year 2020? SQL dates back to 1974, and in the days of serverless functions, running a server also leaves the bad feeling of working with zombie technology here.

The dawn of decentralized

A ghost is roaming web town and its name is “Web3”. A phrase that follows up the early 00’s notion of a “Web2.0” that became a synonym for applications that embrace web technologies and exchange data with their home servers asynchronously aka “AJAX”. Web3 is all about decentralized applications and protocols: not a single server, company or developer should be responsible for uptime, assets and data but the network as a whole. The Web3 concept already has taken huge steps towards production readiness to change the way how we execute distributed code and synchronize trusted application state among peers.

One of the most accepted and proven decentralized application platforms is the Ethereum Virtual Machine (EVM). Even today many people are probably misunderstanding the blockchain based Ethereum platform as a bare ICO-enabling, token mining, crypto-kiddie, ecologically questionable crypto-scam. Or simply put, a Bitcoin on steroids. Only that it isn’t.

At Etherum’s very foundation every node runs a bytecode interpreter. Miners are executing code written into the Ethereum blockchain simultaneously and deterministic while mining new blocks. Clients can add EVM executable bytecode to their transactions to have the miners run it and since it’s written and hashed in blockchain transaction code, it’s unchangeable, untinkerable and unbreakable. Therefore people refer to this kind of code as smart contract: nonnegotiable sets of rules that are carved in stone, forever.

Just a fun fact here: the term “smart contract” has been coined in the 90s by mathematician Nick Szabo, long ago before blockchains ever had been thought of. Some people assume that Szabo is actually the man behind the ominous “Satoshi Nakamato” who once kicked it all off with his Bitcoin whitepaper (but he always denied that since). Yet another fun fact: the trillionth part of 1 Ether is also referred to as 1 “Szabo” as an homage to his theoretical contribution.

When you are going down a step from the philosopher’s cloud, coding smart contracts feels not too unrelated to coding in centralized programming concepts. While you’re free to roll your own, the Ethereum foundation introduced their Turing complete smart contract language “Solidity” as early as of 2014. Solidity smart contracts carry intrinsic state, have public and private methods that can mutate the contract’s state, can emit events, may contain value (expressed as Ether balance), support pure functions and can inherit code from other contracts and interfaces. For most developers Solidity smart contracts should therefore feel familiar: they’re more or less classes. Or put precisely: classes for persistent objects that represent their behavior and state schema. Many people can potentially deploy the same smart contract on Ethereum, effectively spawning instances of it.

a quite complete example of an ERC-20 fungible token contract written in Solidity by Gilad Haimov

Interacting with a smart contract in a state-changing way doesn’t come for free. Miners who execute the contracts’ code must run CPU cycles to execute the compiled bytecode. Ethereum therefore introduces the concept of “gas” to represent fees that are spent on every code execution step. Depending on the operation’s complexity an Ethereum client estimates an amount of gas that’s needed for the computation. At the time of execution a miner translates the gas value to “real” Ethers and expects it to have been sent along with the contract invoking transaction — if the sent funds aren’t sufficient, miners will reject the transaction. Every user who wants to interact with any smart contract in writing direction must therefore pass an appropriate amount of gas with the call.

Smart contracts are unstoppable and enable the authoring of so called decentralized apps (or short Đapps). From a very simplified practical point of view they’re frontend heavy applications that use the Ethereum ledger as event source and database.

Getting started with a decentralized event platform

In early August 2019, during an internal hacking event called “BREAKOUT”, a team of 5 Turbine Kreuzberg engineers, POs and agile coaches formed team “Ethickets” to learn about Ethereum’s concepts and build a simple meetup clone.

Our first idea was to only store a list of RSVPed attendees on the ledger and maintain all the event details in a Symfony based backend application that interacts with Ethereum on a local Geth node through an Elixir based gateway application. A React Native client should then enable users to discover and claim “tickets” for an event. At the event’s door somebody could manually check whether an attendee could present a public address that has been stored in the smart contract.

If you’re slightly experienced with Web3 you quickly will notice the many flaws of our very first approach: the Symfony backend application is ultimately centralized. You could skip the Elixir part alltogether and move Elixir backend interaction code (needed for event creation) to the PHP side. We actually didn’t do so because our Symfony developer felt uncomfortable with it but it’s certainly possible.

Another tricky part of our approach is the usage of React Native: as it turns out running a web3 client that interacts with Ethereum is not the easiest thing to achieve on the native platform: that is mostly due to node.js’ crypto library being incompatible with the web platform and compiling the respective native plugins leads you into the rabbithole of iOS pods and Android JARs to burn in dependency hell. If you really need to interact with Etherum from a web/native client we now can recommend ethers.js, but we didn’t discover that during BREAKOUT week.

We ended up with a working prototype that just ran a simple smart contract like this:

To roughly explain what’s going on here: everybody who’d like to host an event can deploy a new instance of this smart contract thus becoming its recognized owner (hoster = msg.sender). To “buy” a ticket attendees invoke the requestTicketmethod sending along a minimum of one MWei. The ticket price immediately is transferred to the event host. An internal hash table (mapping(address => uint8) tickets) is incremented on each transaction so that one attendee can buy more than one ticket.

The idea was that the centralized Symfony backend would keep track of smart contract instances, effectively allowing event hosts to manage all event details in a centralized way and just utilize Ethereum to sell and maintain “tickets”. Event owners would not necessarily need any knowledge of the blockchain in this concept. As you can see in one of the contracts’ transaction history, this happened several times during Breakout week.

After failing on interacting with Web3 in React Native, we ended up using the brand new React based version of ionic on the frontend and utilized the most well known desktop and mobile (still in beta) Ethereum interaction tool MetaMask aka “The Fox”. Upon startup our mobile optimized web app loads all events from the centralized API endpoint and creates a new Web3 wallet and account by using an injected (in our case: hardwired) private key. From here we could issue requests to the smart contract addresses that the API gave us, effectively being able to “buy” tickets from a fixed user account. We’ve been using the Ropsten testnet via an intermediary infura JSON-RPC node, but we also tested it with our very own geth based Ropsten node — which went equally well. Unfortunately our network didn’t allow us to connect it via Wifi which rendered it unusable for demonstration purposes.

Our smart contract in the Remix IDE with active MetaMask plugin

Intermission: Đappcon

The final eye-opener

What we definitely learned: either you go decentralized or you don’t. Having any centralized part in the whole system would break any other. With that in mind I attended Đappcon in August 2019.

A three day event that completely concentrated on the latest developments in the Ethereum world and it was no disappointment. Hundreds of international guests came to my hometown and I suddenly felt very understood — I noticed that we have not been on some esoteric path on a mysterious alien tech stack, but rather didn’t listen well enough to what so many of Đappcon’s attendees already understood: the Web2.0 as we know it is dead, the future already has begun years ago. Writing smart contacts in a way we were doing could already be considered old school. Decentralized Autonomous Organizations are already (more or less) taking over our understanding of incorporated and limited companies as we know it. I’ve met digital nomads that make a living out of building Ethereum apps and they get paid in Ethereum and Dai (an Ethereum backed Stablecoin oriented on USD). That was my personal turning point: I had to make this kite fly and I had to find a team for it.

The ĐOor — an EthBerlinZwei submission

Only one day later the EthBerlinZwei hackathon was about to take off and boy was I ready to write history. I created a pitch to find people to team up with and found three amazing musketeers to join the party: Tam (Berlin/Israel), Ben (Lithuania) and Niels (Hamburg) happily adapted the idea and we started remixing it. In short we wanted to solve the last part of the puzzle:

how to effectively identify someone coming to your event as a valid (and charged) attendee?

That’s why we called our project ĐOor. It’s potentially usable as a “door opener” for any kind of physical and non-physical entry restriction and it consists of three parts:

  • event management. Everybody can create a new event and control the event’s attendee list, ticket prices and content data.
  • ticket sales / RSVPs. Everybody can discover all created events and signup for them eventually by sending some Ether along as ticket price.
  • the bouncer use case. At the door an attendee’s identity is checked against the list of attendees. If she’s on the list, admission is granted.

We decided to go with Solidity smart contracts again but were using OpenZeppelin tools for it: they come with a huge amount of audited contracts that have been proven to work as great foundation for all common application needs. Further we were building our frontend on plain web technologies and chose to use Vue.js as frontend library (which I personally find a rather unfavorable choice because of the unnecessary conventional complexity when compared to React; but that’s a totally different story).

Niels, Tam, Ben & Stefan = Team ĐOor

Event management

To create new events users interact with a unique instance of a so called DoorFactory that spawns new ĐOor contracts on their behalf. A creator is the first owner of that contract so all fees will be transferred to his account. Thanks to OpenZeppelin’s premade Ownable base contract, ĐOors can be easily transferred to new owners. Here are the more interesting parts of the code, starting with the Door factory contract:

import "./Ownership.sol";
import "@openzeppelin/upgrades/contracts/Initializable.sol";
contract DoorFactory {
address[] public doorAddresses;
event NewDoorCreated(
address indexed doorOwner,
address indexed doorAddress,
string indexed doorName,
uint ticketPrice,
bool allowDisposeLeftovers
);
function createNewDoor(uint256 _price,
string memory eventName,
bool allowDisposeLeftovers)
public returns(address)
{
Door door = new Door();
door.initialize(_price, eventName, allowDisposeLeftovers);
door.transferOwnership(msg.sender);
doorAddresses.push(address(door));
emit NewDoorCreated(
msg.sender,
address(door),
eventName,
_price,
allowDisposeLeftovers
);
return address(door);
}
...
}

and the spawned Door “instance” contracts:

    string public nameOfEvent;
uint public ticketPrice;

uint attendeesCount;
uint256 shares;
enum AttendanceTypes { NONE, REGISTERED, ATTENDED } struct UserStruct {
AttendanceTypes ticketStatus;
}
mapping(address => UserStruct) public users;
function initialize(uint256 _price, string memory eventName)
public initializer payable
{
ticketPrice = _price;
nameOfEvent = eventName;
}

function buyEventTicket() public payable
{
require(users[msg.sender].ticketStatus == AttendanceTypes.NONE, 'User already has a ticket');
require(
msg.value == ticketPrice,
"msg.value does not meet the ticket price."
);
this._owner.transfer(msg.value);
users[msg.sender].ticketStatus = AttendanceTypes.REGISTERED;
}
...
}

Attendance

Every client application (e.g. our Vue frontend) can now discover all existing door contracts that the factory has created by using its contract address, the contract’s ABI and calling its getAllDoors method. Once discovered one can call the Door’s buyEventTicket method and send an appropriate ticket fee in Ethers. Each door keeps track of its own attendee list and relays the money directly to the current door owner.

Ben introduced a twist to the story that goes a little deeper: when you mark an event/door as “withdrawable”, attendees have to pay an upfront deposit, even if the event is free to attend (not unrelated to meetup.com’s idea). As soon as the owner identifies an attendee as having attended the event, the attendee has the right to get his deposit back. That’s quite a clever strategy to decrease no show-rates: if you don’t go to a meetup, your deposit is lost.

The quite popular “kickback” project came up with this idea first and ran quite a bit of events already pushing down no show rates to an all time low. Ben’s little withdraw method allows any attendee to withdraw his deposit after he had been marked as attended by the host and the event officially has ended.

Proof of Attendance

And here comes the hard part. How do you prove that someone attended an event in person? First, standing at the door the attendee has to somehow be able to present her public key since that’s the piece of data that a ĐOor contract saves as indicator for her “ticket”. But what keeps her from simply giving a friend (or the whole world) her public key? After all it’s even stored publicly visible in the blockchain’s history so everybody could simply pick up a public key that had signed on and try to get into the event with it.

The simplest prevention strategy would be to mark a public address that passed the door as “attended” and block every other attendee who tries to use the same address again. However that would mean that we’re now potentially excluding the true ticket holder: first come first enter never has been a good idea, except in biology.

A quite promising variant is the issuing of ERC20 tokens as event tickets. Each token would represent one ticket and to get into the location you simply transfer your token to the bouncer’s wallet. As soon as that transaction is confirmed, the door opens. Good idea, but what keeps you from simply transferring that token to a friend of yours. Shouldn’t be an issue for free events but for exclusive events that might not be exactly what you want. Non fungible ERC721 tokens (NFTs) can remedy that issue but they’re contradicting the reality: besides the number printed on it, a ticket for the next Madonna show is interchangeable. The good thing about NFTs is: you could seamlessly prove which way it went and it’s absolutely unique to the first issuer and owner. A great idea for secondary ticket market applications actually but here’s the caveat: Ethereum transactions are slow.

While on Ropsten testnet it takes around 30 seconds until a transaction can considered as confirmed, on Ethereum mainnet one token transaction might take up to 2 minutes (avg block time of 15s x 7 confirmations). If you’re serving free coffee at your ĐOor that might be an option for someone but surely not for the masses.

As it turns out, asymmetric cryptography comes with a builtin solution to this problem and it’s private key signatures.

Open, Sesame

So here’s our solution: when an attendee knocks at the ĐOor, the bouncer creates a random 4-digit string and presents it to the attendee. The attendee signs that short lived shared secret with his private key and creates a QR code containing the signature. The bouncer scans the QR code on the presenter’s device and calls Ethereum’s ecRecover method with the random string known to him.

checking in with ĐOor

This method will yield the public key of the attendee standing at the door in this moment. He can check that the public key is known to the ĐOor contract (which is a matter of milliseconds since it doesn’t include a status change) and trigger a new transaction to set the addresses state to “attended”. Since all operations happen either offchain or are just reading contract state, the overall verification process takes not longer than 5 seconds and is even faster if the bouncer keeps a local copy of all attendee addresses.

That’s the rough summary on what our team achieved at EthBerlinZwei. Our DevPost submission contains links to all the code and our preliminary demos.

Team Meeting: event management and discovery

While the ĐOor idea is a fabulous tool to check an attendee’s “ticket” to gain admission, one question remains open: should we really manage events and their metadata on that platform?

If we did so, event hosts were supposed to use a frontend owned by us to add new events by a factory provided by us in a format that we have to come up with. We’d finally end up as the next meetup.com as well — with a decentralized database but still with a central concept how event data should look like.

Time for yet another hackathon: our completely new team (with me being the only participant who joined from team ĐOor, the others being Sascha, Jean Daniel, Sebastian and Hendrik) at Diffusion 2019 (October 20th) wrapped their minds on how we could make events discoverable without owning or defining them. Since it’s hard to convince a completely new team to hack on something the team before had left behind, we went with the new name “Team Meeting” and started from scratch.

Use what’s there: the semantic web

For the data part we came up with the idea to go with what’s already available, and as it turns out the ideas of a semantic web is very alive. Most likely out of SEO reasons many sites that display and host event pages are enriching their markup with micro formats or structured data — small but well defined pieces of information, either provided as additional attributes inside the markup, meta headers or meta blocks hidden in script tags and formatted as JSON-LD.

A browser visiting a page that contains schema.org microdata formats immediately recognizes that the user is browsing an event page and could extract all meta data from it: Title, venue location or opening times are all available in a machine readable format. Good for us that meetup.com and eventbrite make heavy use of it: they expose lots of event content on their event pages hoping that GoogleBot makes sense of them. Here’s an example of JSON-LD formatted metadata on eventbrite for the upcoming React.Day:

So why should be GoogleBot the only client to make sense of that data?

We decided to write a Chrome extension that detects microdata formats and extracts event data from pages. As soon as it detects an event, it offers the user an “RSVP” action (or potentially a “buy ticket” button if the event is not free). Once a user hits that button, we’re transmitting the event data to a free, decentralized and globally storage: IPFS.

Thanks to the alpha version of ipfs-js that works without the need to run your own or connect a dedicated IPFS node — peer discovery and upload happens only in the browser (ipfs-js just keeps some constant root nodes to discover peers). Once on IPFS each event gets a unique content identifier, the CID. After some seconds the information is stored on the worldwide decentralized storage network (e.g. https://ipfs.io/ipfs/QmevnNxM2qTRJp6DFdq7Xh2g5GoMXWLoNGAKSjTtKQRv1w)

Since we cannot access a MetaMask / web3 context from within the browser extension’s scope, we’re redirecting the user to a statically hosted Đapp landing page that requests a web3 context and is preloaded with the event page’s original URL and its IPFS CID. Here the user can finally sign up for the event by sending a small transaction to dedicated smart contract’s rsvpForEvent methods:

contract Meeting {struct EventStruct {
address[] rsvps;
bytes cid;
bool isCanceled;
}
mapping(bytes32 => EventStruct) public events;
mapping(bytes32 => uint8) public meetups;
mapping(bytes32 => uint8) public attendees;
event MeetupCreated(string url, bytes cid);
event MeetupRSVPee(string url, address attendee);
function rsvpForEvent(string memory url, bytes memory cid) public {
bytes32 id = keccak256(abi.encode(url));
meetups[id] = meetups[id] + 1; // cheap existence check if (meetups[id] > 1) {
events[id].rsvps.push(msg.sender);
} else {
emit MeetupCreated(url, cid);
}
bytes32 attend = keccak256(abi.encode(url, msg.sender));
attendees[attend] = 2; // code for attending
emit MeetupRSVPee(url, msg.sender);
}
function rsvpForExistingEvent(string memory url) public {
bytes32 id = keccak256(abi.encode(url));
require(meetups[id] > 0, "event needs to be created first");
bytes32 attend = keccak256(abi.encode(url, msg.sender));
attendees[attend] = 2;
emit MeetupRSVPee(url, msg.sender);
}
function isAttending(string memory url) public view returns (bool attending) {
attending = isOtherAttending(url, msg.sender);
}
function isOtherAttending(string memory url, address identity) public view returns (bool attending) {
bytes32 attend = keccak256(abi.encode(url, identity));
attending = attendees[attend] == 2;
}
function isRegistered(string memory url) public view returns (bool registered) {
bytes32 id = keccak256(abi.encode(url));
registered = meetups[id] > 0;
}
function countAttendees(string memory url) public view returns (uint users) {
bytes32 id = keccak256(abi.encode(url));
users = meetups[id];
}
}

That’s a more or less working contract to anchor new events and their attendee list on the ledger. What’s missing is the discovery part (show me events nearby ‘Berlin’). For this we had to dig slightly deeper into the box of secrets:

The Graph: derive a GraphQL API from Ethereum events

Assume you’d like to find nearby events only by iterating Ethereum transactions and fetching their related content one by one. If the amount of stored events grew large it’d be a massive overhead to do so on every request and that’s why one has to come up with a (potentially centralized) projection that persists the event store’s generated state in a queryable database. If we were only to index the event data on IPFS, OrbitDB could be a suitable option but since we are about to update the projection as soon as an RSVP occurs, we decided to go with the The Graph.

GraphQL API inferred by events emitted by our smart contract

The Graph is a YML-configured GraphQL-SaaS / self hosted service that listens on events on Ethereum and IPFS and updates its internally persisted state according to the event data. You can write handlers and mappings that are invoked as soon as an event is triggered and within these you can do nearly anything you need to fetch the data that’s related to the event. The Graph mappings are written in AssemblyScript, a subset of TypeScript and compiled to an executable WASM binary.

There are two mandatory parts to configure and build your Graph instance: first you’d want to define a GraphQL Schema definition for the projected entities. The Graph is inferring an ORM like entity layer that you can use inside your handlers to read and write entities to The Graph’s provided “store” abstraction. The most important part is the “subgraph” configuration that The Graph uses to setup event listeners for your smart contracts and bind your custom handlers to events and entities:

Being configured like this, The Graph will setup services that listen on MeetupCreated or MeetupRSVPee events of the Contract contract and feed all that data into a hosted database (which actually uses Postgresql under the hood) with an automatically generated GraphQL API layer on top of it. The frontend can simply ask for deep graph data, e.g. to filter upcoming events with specific tags in a certain city.

Our final Devpost submission for “Team Meeting” can be found here. Currently the code is still in hackathon state but works more or less as explained above.

Consolidation & Conclusion

What we were building and what we’re about to build

All the code and facts that you’ve seen above are the result of three weekends without sleep, achieved by around 10 people who never have met before in their lives. So please bear with us: this is not a working piece of software yet.

At the time of writing we’re in the middle of figuring out how we continue this project. We most likely stay on the Ethereum chain, even though there’s a lively discussion going on whether Parity / Substrate, NEAR, elastic Eth Sidechains or Eth2.0 / Serenity are the stacks to wait for or build on. Personally I’d happily stay with the proven Ethereum stack and embrace MetaMask as “identity” wallet and mandatory dependency for interactions.

We will surely invest some energy in the event detection / storage / discovery part of the whole story since this is what the most owned, federated and siloed part of the current meetup/event ecosystem is. The people behind our ĐOor idea want to take it a step further and add more close-to-event features (e.g. Kickback-like RSVP deposits that will be transferred back to their origin as the proof of attendance had taken place) whereas Team Meeting prefers to decouple functionality as much as possible to just help with the plain chain anchored event data base.

If you want to be part of the discussion or would like to dig through or contribute to our code or you want to fork it and build your own new meetup.com: go ahead, the way is free for you — here’s our “ ĐOor” organization that currently maintains both our ĐOor and Team Meeting repos: https://github.com/d0or . If you’d like to support or integrate us in any way, don’t by shy and contact us.

t14g

The future runs faster than the present. t14g is run by the fine people working at Turbine Kreuzberg.

Stefan Adolf

Written by

Developer Ambassador for Turbine Kreuzberg & señor fullstack developer: Symfony, node, Mongo, Solidity, Gatsby, React. Lives to code — dies for Rock’n’Roll 🎸.

t14g

t14g

The future runs faster than the present. t14g is run by the fine people working at Turbine Kreuzberg.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade