Building MutualAidIndia.com with Gatsby, Airtable & Tailwind
In April 2021, while much of the world was starting to see a light at the end of the COVID-19 tunnel, India was in the middle of a health crisis of nearly unimaginable proportions. During the last week in April, the country averaged over 300,000 cases per day, and that number is likely a massive under-count.
There were shortages of just about everything – especially O2 beds in hospitals – and many wage labourers losing most or all of their income. For family health reasons, my household was on near-complete isolation, but you couldn't avoid reality. My timeline was flooded with requests, friends retweeting leads for supplies; some tech companies stopped their work to build directory apps to help supply meet demand; municipal government hotlines were flooded with calls asking for the right place to go to get the help people desperately needed; heroic efforts from volunteers and self-organising collectives to respond to the pleas for help and call every hospital in town on behalf of patients with more limited time and resources.
This post is about the technology we used to build the site MutualAidIndia.com, but the human factor, the crisis and desperation, are a crucial part of the story.
After a few weeks of doing this gruelling volunteer work and dealing with the second-hand trauma of the unfolding health crisis, a friend of a friend named Riddhi decided they would change tacks and focus on a different problem: during the lockdown, lots of people would die of hunger, or lose their homes, or the shortage of spots in government hospitals would mean that the poor may be unable to afford treatment in private facilities.
What people needed was money. Big INGOs were accepting money into their bank accounts in London and New York, paying their staff and funding programs here that would take weeks or months and often focus on things far less urgent than basic survival. When news broke of critical Oxygen shortages in Delhi, one major fundraising site partnered with a bunch of big NGOs and raised millions to buy Oxygen cylinders – this of course did little to increase the actual supply of Oxygen cylinders, which are produced in industrial facilities that don't scale up and down quickly; it just meant that the urban hospitals that had tie-ins with this NGO could now dictate where the Oxygen went.
It was easy to feel like we were all just re-arranging deck chairs on the titanic. So, fed up with basically everything, Riddhi started a list. It started as a tweet.
Hi, I'm getting a lot of questions around whom can we support financially. Want to collate
> Can you pls comment on this thread if you know of a good organisation doing verified relief work/ supporting vulnerable communities / figuring food and essentials for migrant workers etc?
And within a few hours it was a google doc, with an ever-growing list of fundraisers that needed attention, and a small band of volunteers jumping in to help.
I saw Riddhi's tweet and request for help the next day, and volunteered to help verify which fundraisers on various platforms could accept foreign contributions. It was straightforward enough; i joined the gdoc and started verifying fundraisers.
Within a few days traffic on the doc was so high, Google had turned off a bunch of features and was permanently showing us the "please use our 'embed' feature instead of linking people directly to the doc" alert. We were editing the doc live with a team of about a dozen volunteers. And with an aggressive social media strategy I lovingly describe as "shaming influencers," we had several days of very spikey traffic, cresting at around 50,000 visitors a day.
Our Emerging Product Needs
The list was taking off and we were quickly getting a sense for our needs:
- A scalable mobile-ready web interface (not Google Docs)
- A more structured workflow for editors / people updating fundraisers
- More structured data store to keep track of fundraisers, beneficiaries, and the times our team would contact them to ask them how their fundraiser was going (remember a lot of these didn't have a website; they were just posting their GPay ID)
- An easy way to tag and/or filter different fundraisers as accepting foreign contributions, as urgent, as having met their goals, etc.
- An easier way to keep a "highlighted campaigns" section up at the top, and rotate them regularly
- We needed it to be visually very simple, but to feature some of the amazing art that our volunteer team was producing and which had been central to our viral success
- Bonus points:
- Easy one-click GPay or UPI links (in India, GPay is one of the most common e-wallets)
- Some kind of analytics, maybe!
Oh, and a big one:
- The entire thing had to be completely free.
We weren't taking or raising a single Rupee in operating costs, and every time someone offered to spend a few bucks to pay for our hosting we sent them a link or a GPay ID and said "donate here instead". Heck, at times I was tempted to do the same, and every time I would just open up the list and donate ₹1500 to the top item on the list.
I know this isn't usually how we like to make tech and product choices. Usually it's fine to spend ~20 USD to amplify efforts raising thousands, but our whole thing was that we were different from traditional NGOs; we were offering people who didn't feel comfortable buying into a sort of corporate-ized charity scene something entirely different: the opportunity to engage in and support mutual aid. And this wasn't just a vanity requirement, it felt like a part of why we had been so successful getting influencers to speak out, particularly on Instagram. So we accepted this zero-dollar product requirement and worked from there.
Options considered
We had a meeting; gathered all the inputs, wrote up a brief and some options, and considered the following:
- Some free hosted WordPress site (might require a WP person because I don't know WP well)
- GlideApp which lets you build a site based on a Google Spreadsheets back-end (very attractive option)
- We could roll our own site using some low-maintenance static site generator (I had just gotten a bunch of experience with Gatsby, so that was one option)
We ended up kind of going in a few different directions with this, putting out a call for volunteers to start prototyping each of the sites. I took the Gatsby/Airtable site; worked with another volunteer to make the GlideApp version; and we never got the WordPress site off the ground. Regardless, we discussed the tradeoffs with the team, and within about 5 days of it becoming clear we needed this website to exist, we had made our choice: we would go with Gatsby + Airtable.
Building The Site
As I generally don't consider myself "a developer," I was really only prepared to do the simplest possible version of this site. So I went looking around, of course, for tutorials and 'splainer articles and sample code. The best source was actually the Netlify Examples Directory, where I found this site: a travel destinations listing built with Gatsby, Airtable, fetch, and TailwindCSS.
For me, this was absolutely perfect. I had worked with Gatsby but didn't know how to set it up. I had never used Tailwind before but was already looking to try it out. I had used React+Airtable before for a hobby project, but didn't want to have to figure out how to make gatsby-node.js
work from scratch. This example site took care of all this getting-started work. All I had to do was tear it up: I know how to edit layouts, make little "badge" components, reshape an Airtable, create views and write how-to guides for volunteers to get onboard.
What worked well
Art and Soul
Thanks to the artwork of our volunteers and particularly our resident artist Sonaksha, we were able to showcase the kind of art that had helped spur the original viral success of the site.
As an example, here is the cover art for the Zine – nay, a full e-book! – that Sonaksha and Riddhi wrote for one of our fundraising pushes to help raise funds. It's just beautiful, and so human, and kind. It captured what we were all feeling.
Tailwind
Tailwind was a joy to work with. I love the separation of concerns (and sometimes the lack thereof), and I love the way they put configuration in javascript and then let the CSS just do styles.
Tailwind CSS is a utility-first CSS framework that has become all the rage recently, especially for folks using React, and for good reason: In react, you separate components into different files, and each component contains its own markup and business logic, and often its own styles too. With Tailwind, you write extremely granular classes like my-4 md:my-6 bg-red-600 text-white absolute top-0 hover:underline
. Each of these classes basically maps onto just one or two CSS rules (or in the case of md:my-6
a media query and then a margin-top and margin-bottom).
So why not just write bare CSS? Well, Tailwind uses PostCSS to apply all the browser prefixes you could ever need, and then "purges" the CSS output to include only the classes that are actually used in your application, so it's close to the same bundle size as writing bare CSS, and a lot less error-prone. It's also a lot quicker to type my-4
than margin-top: 2rem; margin-bottom: 2rem
and easier to read. It provides good support for transitions, gradients, dark mode, and more.
Now, I had already started adopting something close to this approach with recent releases of Bootstrap, which had started offering many more utility classes, as well as grid utilities that made it easier to break out of the pre-defined layouts and components and just make your own. But I was doing it with SCSS, and relying on SCSS to create my own branding and design language. What I loved immediately about Tailwind was the choice they had made to define their post-processing behavior into a tailwind.config.js
file, where you can rename colors ("blue" -> "primary"), define new font family classes like "body", "slab" or "serif", and customize your colors by choosing from a library of variants like "pink", "rose", "fuscia", and so on.
This, for me, took it from being a CSS "library" to a real framework. It meant I wasn't making my own decisions about whether to use variables.scss
to define my own color values and paddings, and then figure out how to use the pre-written mixins to extend various utilities; I just had to configure the processor, and it would generate all the classes I needed.
Anything but a Google Doc
Moving off of the unstructured text of a Google Doc led to immediate gains in productivity, lower time to edit and update fundraisers, fewer errors and easier inspection of what went wrong when errors did crop up. Airtable gives you creature comforts like a "last edited" field, and even lets you set more specific conditions like "when was the last time someone edited one of the following 6 fields...".
Down sides: Airtable, at least the free version, doesn't give you the ability to apply validation rules to your inputs, so we could say "this field is a URL" and it would only accept inputs that looked like URLs, but we couldn't say "this field is a slug and it must match this regex" or "if the record has the 'google pay' tag then the 'gpay_account_id' field must be filled in". It seems this is just the kind of power and flexibility you lose when you are not in control of your own content management. So you have to handle these things with training, keeping the team small, and reviewing records frequently.
CloudFlare Pages
We hosted the project on CloudFlare pages, which has an extraordinarily simple interface for developers, connects straight to your GitHub repo, and builds a new version of the site any time you push a commit to any branch.
Down sides: At the time, you couldn't have it automatically rebuild the site every few hours. So a fellow volunteer set up a Github Action to push an empty commit every 2 hours.
Bonus up side: The CloudFlare discord was really helpful and I had a couple questions answered directly by CF staff!
React Hooks-based Components
I had never used Hooks in react before, but I was ready to get started! I never liked or trusted the old class-based approach so I was lucky that the repo I was copying already had a few examples of useState and useEffect, as well as one custom effect that combined both. I was able to build on this working sample code and adapt it to my own needs and had a fantastic experience learning this new paradigm.
Because I'm not very technical, I often struggle to learn new things when I don't have a mentor around, but this kind of very simple listings-page website was a great way to dip my toe in the water. For example, this DismissableAlert component only needs to know whether it has been dismissed or not, and needs to be able to dismiss itself. So the opening lines of the component's code look like this:
export const DismissableAlert = () => {
const [isAlertClosed, setIsAlertClosed] = useState(false)
return isAlertClosed ? null : (
<div className="relative bg-yellow-100 py-5 px-8">
<button className="absolute top-0 right-0 p-3"
onClick={() => setIsAlertClosed(true)}
aria-pressed="false">
✕
</button>
Simple enough, right? We just use React.useState to declare a state isAlertClosed
and when someone clicks the "X" we call a "set" function to close it. This is exactly the kind of easy on-ramp that a dev like me – without a ton of React experience and no senior developer to guide me – was looking for.
What Didn't Work So Well
Gatsby
When Gatsby was released it was heralded as the next best thing to happen to websites. And in terms of popularizing the JAMstack approach to web dev (Javascript, APIs, and Markup), it kind of was. It's a static site generator that has a rich plugin architecture to manage things like your <head>
section, image scaling and thumbnails, Google analytics and outbound link clicks, rendering markdown, and so on.
But my experience was that I had landed right back into the kind of WordPress'y hell where you have 5 community-supported plugins that do the same thing, and the official plugin hasn't been released yet or has been deprecated in favor of something else.
But what really slowed me down was Gatsby's insistence on making me use GraphQL for everything. I'll be upfront: I don't know GraphQL. I understand it seems like it is meant to perform a valuable function – let devs issue queries to the database/API and be very specific about what fields and/or nested objects they want it to return.
But I absolutely do not understand why I need to use some Gatsby plugin-specific code nested inside a graphql query in order to fetch and display an Image. Plugin, I just met you. We are not friends. I am just copying and pasting code from the docs. Thank goodness most of the fetch-and-display logic was written into the site before I found it because, although I could vaguely explain what this code ends up doing, I really couldn't explain how it knows to do that.
I even managed to re-use the fragment in several places, but, what is happening here? There is some type declaration happening too? Am I using TypeScript now also?
Suffice it to say, I tried two or three times to upgrade the Image plugin to the non-deprecated one recommended for Gatsby-3, but it had a different way of going about these graphql queries and I was never able to figure it out. This approach to managing data means that just to feel even slightly comfortable using Gatsby the developer has to:
- Use GraphQL which there's a good chance they've never used before
- Understand how the "export" directive works and also how Gatsby will use it for other pages and components
- Understand how Graphql uses parameters and types, in a base-level-syntax kind of way that can actually be tricky if you don't have guidance
- Understand how your particular plugin works and creates Graphql functions or fragments that we don't ever seem to explicitly import but which are there and are mandatory.
You also have to figure out gatsby-node.js
, which, even for a very simple site like ours, looks pretty strange, like a weird mix of configuration and code. (And what is this return new Promise((resolve, reject) => { ... })
? I know about async/await but this is another level.)
Months later, after about a year of working with Gatsby, some of it with support from senior engineers, and several months working with a much more sensible static react site generator (NextJS), with stronger fundamentals in es6 and promises, it all makes a bit more sense, but for a developer uncertain about any or all of these issues, I would just say don't even touch it, or find yourself a framework where generating paths and fetching state site data makes a ton more sense.
Airtable is Not a CMS
Airtable was actually really convenient for keeping track of the fundraisers, tagging them, keeping track of their goals and when they'd met their goals. But it's not a CMS. The lack of basic validation features, mentioned above, is aa big hindrance when the team grows beyond the very small set of people I speak too on a daily basis.
And perhaps more to the point, when we wanted to add new pages with custom content, while it was possible to add arbitrary HTML to an Airtable field, how would you even structure a page with, say, 4 sections on it? Would you make a Table called "sections" and each section has a heading and content and a field for "what page does this show up on" and/or "what order does it appear on that page"? Or would you give each fragment a unique name or slug and then write pages that just for "display this fraagment, next display that fragment", so you're sort of hard-coding the layout and order of things, but not the words themselves?
I understand that the paid Airtable has some options for improving on this, but I would much prefer to use a system that is meant to manage content, like using a headless WordPress or a system like Contentful.
In theory, a JAMstack site like this one should make it easy to pull fundraisers from Airtable and then build content pages from the CMS – e.g. a featured campaign with a more in depth story about that particular fundraiser – and have the content pages include up-to-date data about the campiagn in question, pulled from the Airtable. But then again, I might have to promise my firstborn child to the gods of gatsby-node.js in order to make that work. (jk, I think I know where I would do it.)
Conclusion
The Mutual Aid India site was a joy to build, perhaps most directly because I was working with a really clean code base of React and Tailwind, that felt new and fresh and still mostly comfortable, and because the requirements were pretty straightforward and didn't require a complex back-end or a larger team. And because the team I was working with, and whose project I was building all of this for, was full of such incredible people with such great hearts and sharp minds.
We got the site off the ground in less than a week, while the second wave was still peaking and attention was high, and we were able to raised tens of thousands of dollars that went directly to people in need.
We got to show, and practice, a new way – actually, a much older way – of helping others. We didn't ask them to send us receipts of their purchases or make poverty-porn videos to post on social media, and we attracted enough notoriety to help them tell their stories to the press, and to explain why supporting mutual aid is important for everyone to do. We treated everyone with dignity and respect, like full human beings with a right to live, to survive and so much more.
I learned so much from this project – not only about making websites but about myself and the people around me. I made friends I respect and admire, who built a community out of the darkness and the thorniest pieces of our shared feelings of isolation, disability, and despair, and whom I hope to stay close with for a long time. Here in India, the pandemic continues, though the media-and-influencer attention has moved on to other things. But I am proud of our work, our team, and the help we were able to provide. 10 out of 10, would build it all again.