Casting off for new shores

One thing I’ve learned this week – even if you know about the sunk cost fallacy, it can still somehow creep up on you unexpectedly.

Writing has been a lifelong dream for me. For about as long as I can remember, I’ve always been fascinated with stories, and would constantly dissect and re-imagine them in new ways.

But it never felt like the right time to actually write. The reasons are varied, complex, mostly depressing, some aggravating, and all in the past – the sum of it is that I’ve never really been able to settle on something that I actually wanted to do, and as a result of that paralysis, have done largely nothing.

Last year, May 2016, I redid this blog – archived all the old content, changed the domain name (supremely happy to have landed wogan.blog, let me tell you), and wrote this introductory post.

Fast forward a year, and while I’ve done some measure of that privately, I haven’t done nearly enough of it publicly. What I have done, instead, is retreat back to the same comfort zones I’ve always had.

 

Towards the end of 2016 I started getting agitated that I wasn’t making progress with my writing, and decided to tackle it as a pure productivity problem. People are writing and publishing books every day – there has to be a system that will work, right?

That’s where the idea for Write500 came from – my own desire to set specific goals that I could hit, every day, and make progress as a result.

But then my comfort zones kicked in.

When I talk about the “sunk cost fallacy”, the first assumption you might make is that I’m referring to the time I spent on this specific project – which is a factor. Over the last few weeks I’ve been working on Write500 more or less because it exists, and because I know there are people that are interested in how it evolves. Not so much because I think it can actually solve the problems I need it to solve.

So that’s one level of fallacy right there – working on something because I’ve been working on it before, and the cost of killing it is somehow (inexplicably and irrationally) unacceptably high.

But there’s another level to this, and it’s only in this last week that it’s really been driven home for me: My career so far is, itself, a sunk cost.

My approach to solving problems is almost always rooted in software, which shouldn’t be a surprise – I learned to program at a very young age, and because of my knack for it, I was able to get a job, which I was then able to turn into something resembling a career.

As a result, when I’m deciding the best way to add value to the world (which I believe is something we should all try and do, in our own way), my main inclination is almost always software. I keep coming back to it, thanks to how far its gotten me in life so far.

Over the last year though, that inclination’s been challenged somewhat. Last year around this time, I was in the process of handing over my biggest freelance client to another agency, thanks almost entirely to burnout.

The idea of working late nights developing software for paying customers had lost its appeal entirely, despite how absolutely brilliant it was to be able to monetize my nights and weekends, and build up some savings as a result.

I’ve had a similar inclination at work, too – whenever a new problem presented itself, and if it could be solved with some custom development, that would always be the first suggestion I’d make.

Which had not been a problem, really, up until the end of 2015 – I had latitude to develop things that I thought needed to be developed. The change of my job role had also forced me to change the way I solve problems – including occasionally not solving them at all.

I imagine it’s at this point that a lot of developers would quit out of frustration – feeling like they’re adding no value, or taking umbrage at not being able to use or grow their skills.

I didn’t quit, though, and I ended up learning something new: That it was possible for me to be productive (and add value) without writing a single line of code.

I’m sure that seems obvious to a lot of people, but it’s only recently become clear to me how big of a mental shift this actually is. And it brings me back to the thing about writing.

I’ve wanted to be a writer for as long as I can remember – the idea of spending time in my own world, creating characters and stories within it to share with other people – is incredibly appealing.

But with my very narrow view of problem-solving, I’d always look at my lack of writing as something that could be solved with software. And so instead of actually writing, I’d set out to shave as many yaks as possible.

It’s the old “if all you have is a hammer” adage – the problem of me not writing started to look like a nail. A problem that could be solved if I just found the perfect combination of tools, frameworks, and the right approach.

Which as it turns out, is horribly wrong – at least for me.

Most of my work on Write500 was underpinned by that. The first, most basic thing it was meant to do was deliver daily writing prompts (a tool I always wanted to build anyway). But beyond that, I wanted Write500 to solve two other problems: Be a daily go-to tool to produce new content, and be a revenue-generating SaaS product.

Except that neither of those things (and it’s obvious now) actually move me any closer to me being a writer. It’s actually the exact opposite: I’m creating new tasks for myself that specifically prevent me from writing, but justify it by telling myself that once I build this, I’ll be equipped to write.

Which is bullshit, and I think I always knew it was bullshit, but I let myself believe that anyway.

Another big dimension to all of this is that I’m doing all of this work in my spare time. What little of it I have, anyway. Time to work on these sorts of things is a scarce resource for me, and I haven’t been making very good use of it by focusing almost entirely on things that move me in the exact opposite direction of my goals.

And so last night, while processing all of this (and failing to fall asleep) I came to the eventual realization that I have to kill Write500. Specifically, the extensions to it – the daily prompts thing is still quite useful, and low-maintenance on my part.

Once I actually go through the process of producing and publishing something, I’m sure I’ll uncover lots of problems from that experience – and I might find a gap that could be filled with software.

For now, though, I’m rolling everything back and parking this project. A part of me still hates to do it, but the reality is that I have limited time available to me, and I’m not actually making the progress I want to make.

Instead, I now know I should be focusing on the things that are outside my comfort zone: Namely, writing things I think are shitty, and sharing them with people that might have nothing good to say about it. Which will be a start 🙂

Mid-year Resolutions

It really does feel like January was just the other day – hard to believe we’re already halfway through 2017. It must be some sort of bad joke.

The passage of time hit me properly today when a conversation about cryptocurrencies came up. I remembered that just a few weeks ago, I had dumped a bunch of money into a local exchange and bought some, and I had abandoned that exchange after an issue over support.

(A few weeks ago == January. It’s been five months, but we’re not mentioning that.)

I logged in to find out my R800 investment had somehow turned into R4700, thanks to the exploding value of Litecoin. I could have had an even bigger return had I logged in just a few weeks prior, but I was happy enough with the 500% growth over 5 months.

That experience brought me back to January, for which I still had the unresolved support case sitting in my inbox, along with a pile of notes and plans and ideas. I had sketched out a few things I wanted to achieve in 2017, and it feels like I’ve just now come up for air and half the year is gone.

Among the many things I’ve wanted to do, is actually get a book published. It’s why I started Write500 in the first place, and it’s why I’ve tried to build a daily writing habit. I’ve done okay in some weeks, badly in others, but still not enough to actually get a first draft done.

It reminds me (rather annoyingly) of a conversation I had back in 2007. Back then, at the ripe old age of 18, I had already decided I was going to write and publish something, and declared I’d have it done by November of that year. So naturally, someone went and marked that up on a calendar.

November 2007 came and went, no novel.

And now it’s ten years later.

It’s my fault, really, for not putting writing and writing-related activities higher on my list of priorities. I figure if you’ve passively thought about doing something for more than ten years, it’s probably something you actually have to do, right?

So the first thing I’m going to try doing differently, is writing here a lot more, not averaging a month between posts. I’ve always tried to have a topic, theme, structure, and something useful to say before saying it – and that’s led to me more or less saying nothing, since the bar to actually blogging is set so high.

The other thing I’m going to do is actually try pulling away from tech a lot more. I decided ages ago that I needed to spend a lot more time on creative work, and yet somehow the majority of the content posted so far this year is tech-related.

I guess old habits are really hard to break!

So for this 2017 Mid Year’s Resolution, I’m going to try writing more frequently, and less about technology. That seems like a good enough place to start.

Write500 and the Abyss of Reluctance

The story of Write500 so far can be best summed up in this commit chart from Bitbucket:

Screen Shot 2017-05-30 at 6.11.39 PM

The project started in December 2016, with the intention to create a tool to help writers write every day. At the time, I was pretty optimistic about my ability to retain focus:

…if I can finish this off as intended, I’ve got some other feature ideas to throw in. But right now, I shouldn’t get distracted 😉

I wrapped up and launched the first version before December drew to a close, and I was able to open registrations on 1 January 2017 – ringing in the New Year with a new project.

I then engaged the marketing engines, trying out a few different formats, and it wasn’t long before I reached the 500 subscriber mark. That, too, was hugely motivating – and it was less than a week later that I created the write500-app repository, starting work on the “real” version of Write500.

This was going to be the web app that I had originally envisioned, plus a ton of new ideas that I had picked up over the first few weeks. It was going to be an all-in-one of sorts: Writing and statistics, a built-in social feature, built-in community, and enough features to support two pricing tiers.

At the time I wrote my 28 January update, I had not actually done any creative writing since the 13th (about the time I started planning out the full app). I figured it was okay to let that lapse, since I was focused on building something that would eventually help me get back on track.

That momentum carried all the way through into the first two weeks of February, which is when the inertia set in. It’s pretty clear from the chart above: my code commits to the project simply fell flat. Looking back on it now, there were two main reasons:

Feature Fatigue

Too much, too early. I had actually managed to build up (at least, in my head) a beast of a system, but for every new feature I added, it felt like more features were missing. Pretty soon, the lists of things I planned on implementing had eclipsed the intention of the project itself (seriously, I was reinventing activity feeds towards the end).

I could actually start to feel the drift: More and more hours were being spent productively, but it felt like every new commit was pulling me further away from launching a usable product. Worse, it was getting harder for me to justify why the code I was working on, would actually help writers write more.

In the space of a few weeks, Write500 turns from an exciting project, to something resembling dread – towards the end, I just couldn’t bring myself to open the IDE and carry on working. I was firmly in the Abyss.

No Dogfooding

I had stopped writing every day – the irony of which does not escape me. At some point it became more important to me to work on Write500, than it did to actually write every day – the very problem Write500 purports to solve.

As the necessity to write every day started to wane, so did my motivation for solving the problem. It was a spiral I managed to trap myself in pretty effectively – as the scope of the product expanded, and my capacity for daily writing diminished, I thought I could solve the problem by designing extra features.

I had lost sight of trying to solve the problem for my hypothetical users in the simplest way, and was instead trying to solve a problem that existed entirely in my mind.

With the marketing campaigns long-expired (and subscribers only trickling into the free list), and my own capacity for writing eaten up by software development, that vicious cycle finally ground me down to complete ineffectiveness around mid-February.

It’s usually around this point in my projects where I just give up – I decommission, archive, and shelve, chalk it up to my inability to stay on-target, and move on. And I came close to doing that several times.

How is Write500 different? In the end, I think it had everything to do with this chart:

Screen Shot 2017-05-30 at 6.36.35 PM.png

After almost 5 months on the daily list, less than 25% of users had unsubscribed. More than 600 people were still getting value out of those daily prompts.

That chart gave me a different perspective on the problem entirely. Where I had been trying to solve problems with introducing ever-more-complex features, most users to date had simply been carrying on with the free list.

Maybe I was over-engineering it? That thought only occurred to me around mid-April. Maybe it would be possible (even, desirable) to throw away everything except the core experience (getting a new prompt every day), and basically start over.

The Great Purge

And so on 11 May 2017, I started doing just that.

Screen Shot 2017-05-30 at 6.42.57 PM.png

I gutted the entire project – all the controllers, views, models, migrations, resources, assets, most of the configuration, most of composer.json. And then I started over.

By the end of the first day, I had re-implemented the basics – authentication, prompts, the basic writing interface, the streak display. All perfectly-functional components of the behemoth project, brought over more-or-less intact.

The remarkable thing here? In the old project, those exact same components felt like smaller by-products of a larger vision. In the rebuild, with a fresh perspective, they actually felt like core components again. I found myself able to chart a much clearer path between the code I was writing, and the value I expected my users to be able to get out of this.

A week later, I had the subscription mechanism and Paypal integration restored, and documented better than before. I added a new Statistics mechanism, which now tracks and records wordcount and speed pretty much in realtime. I added the export options which were initially high on my list of priorities, but had fallen by the wayside.

This purge-and-refactor process brought back all the motivation I had lost before. Write500 transformed again – from something seemingly without end, to a project I could conceivably finish.

All the commits from the 21st onwards were mostly cleanup and polish – fixing typos, rearranging screen elements, testing in Browserstack (unbelievably useful) for the major mobile devices, adding a streamlined migration onboarding path from the free list service, and so on.

This past Sunday (the 28th) I rounded it off by adding the Terms and Privacy pages (TermsFeed.com was enormously helpful for the former), and finally pushed the v0.3.0 tag. I did this while sitting in a hotel room in London, having just landed a few hours before.

On Monday (thankfully, a bank holiday in the UK), I was able to refine and run the new dispatch system, and gave it a full day to test. And today, I completed the migration of all users from the free list service to the new version of Write500.

Less really is more

There’s an excellent quote – the origins of which I have long since forgotten – which I routinely forget to apply to my own work (and I’m paraphrasing a bit):

Every project eventually exceeds the developer’s capability to maintain it.

Write500 outgrew my ability to maintain it even before it had made it out of my dev environment – which is not smart, and is the reason I failed to launch it in the first place.

With limited time and resources, the smartest approach is almost certainly the leanest one. The version of Write500 I have deployed right now (0.3.5) is a far cry from the vision I have for it, but it has one compelling thing in its favor: It exists.

 

Carrying on

It exists, but it’s also only getting started. The real test is whether or not there is actually a market for this. I’m happy with the way the free list has performed – there’s clearly some demand out there for tools that make consistent writing easier to achieve.

Is there enough demand, though, to turn this into a paid product? I guess only time (and marketing!) will tell.

At the very least, I’m glad to have been able to make this amount of progress. Write500 is the first project that actually came back from the Abyss of Reluctance, and made it into production.

Which, right now, is enough for me!

Getting started with Mastodon!

Mastodon Setup

Howdy, stranger! This document is the other half of this video, in which I set up a single-server instance of Mastodon. This was assembled on 9 April 2017, and there’s a good chance that some of the specifics here will change over time. I’ll keep an updated version up on wogan.blog.

(What is Mastodon? I’ll do another post on that sometime!)

If you’d like, you can download a plain HTML, styled HTML or PDF version of this post instead – it might make copying some of the code easier.

UPDATE 17 April 2017: Mastodon has reached v1.2, and now requires Ruby 2.4.1. The post has been updated with new commands required as of today, and an upgrade guide is below.

0. Pre-Prerequisites

At a bare minimum, you’re going to need:

  • A domain name, with the ability to add an A record yourself
  • A free mailgun.com account, with the account verified and your sandbox enabled to send to you
  • A 1GB RAM machine with decent network access. This document uses a DigitalOcean VM.

This setup procedure skips a few things that you may want to do on a “productionized” or “community” instance of Mastodon, such as configuring S3 file storage, or using a non-sandbox email send account. You may also want a beefier machine than just 1GB RAM.

For reference, the OS version in use is Ubuntu 16.04.2 LTS and all the commands are being run from the root user unless explicitly specified.

1. Getting started!

The first couple steps:

  • Create the VM
  • Point your domain to it immediately, by setting the A record to the public IP
  • Log into the VM
  • Set your root password
  • Create a new Mastodon user: adduser mastodon
  • Update the apt cache: apt-get update

2. Install Prerequisites

Now we’ll grab all the prerequisite software packages in one go:

# apt-get install imagemagick ffmpeg libpq-dev libxml2-dev libxslt1-dev nodejs file git curl redis-server redis-tools postgresql postgresql-contrib autoconf bison build-essential libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libncurses5-dev libffi-dev libgdbm3 libgdbm-dev git-core letsencrypt nginx

That’ll take a little while to run. When it’s done, you’ll need Node (version 4) and yarn:

# curl -sL https://deb.nodesource.com/setup_4.x | bash -
# apt-get install nodejs
# npm install -g yarn

You’ll also want to be sure that redis is running, so do:

# service redis-server start

3. Configure Database

With Postgres installed, you need to create a new user. Drop into the postgres user and create a mastodon account:

# su - postgres
$ psql
> CREATE USER mastodon CREATEDB;
> \q
$ exit

Later on we’ll configure mastodon to use that.

4. Generate SSL certificate

Before configuring nginx, we can generate the files we’ll need to support SSL. First, kill nginx:

# service nginx stop

Now proceed through the LetsEncrypt process:

  • Run letsencrypt certonly
  • Enter your email address
  • Read and acknowledge the terms
  • Enter the domain name you chose

If the domain name has propagated (which is why it’s important to do this early), LetsEncrypt will find your server and issue the certificate in one go. If this step fails, you may need to wait a while longer for your domain to propagate so that LetsEncrypt can see it.

5. Configure nginx

With the SSL cert done, time to configure nginx!

# cd /etc/nginx/sites-available
# nano mastodon

Simply substitute your domain name where it says example.com in this snippet (lines 9, 15, 23, 24), then paste the entire thing into the file and save it.

map $http_upgrade $connection_upgrade {
  default upgrade;
  ''      close;
}

server {
  listen 80;
  listen [::]:80;
  server_name example.com;
  return 301 https://$host$request_uri;
}

server {
  listen 443 ssl;
  server_name example.com;

  ssl_protocols TLSv1.2;
  ssl_ciphers EECDH+AESGCM:EECDH+AES;
  ssl_ecdh_curve prime256v1;
  ssl_prefer_server_ciphers on;
  ssl_session_cache shared:SSL:10m;

  ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

  keepalive_timeout    70;
  sendfile             on;
  client_max_body_size 0;
  gzip off;

  root /home/mastodon/live/public;

  add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";

  location / {
    try_files $uri @proxy;
  }

  location @proxy {
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;

    proxy_pass_header Server;

    proxy_pass http://localhost:3000;
    proxy_buffering off;
    proxy_redirect off;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    tcp_nodelay on;
  }

  location /api/v1/streaming {
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;

    proxy_pass http://localhost:4000;
    proxy_buffering off;
    proxy_redirect off;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    tcp_nodelay on;
  }

  error_page 500 501 502 503 504 /500.html;
}

Once you’ve saved and closed the file, enable it by creating a symlink:

# ln -s /etc/nginx/sites-available/mastodon /etc/nginx/sites-enabled/mastodon

Then test that the file is OK by running nginx -t. If it reports any errors, you’ll want to fix them before moving on. If the file comes back OK, fire it up!

# service nginx start

Open a browser tab and navigate to your domain. You should get a 502 Gateway Error, secured with your LetsEncrypt cert. If not, go back and make sure you’ve followed every preceding step correctly.

6. Configure Systemd

Mastodon consists of 3 services (web, sidekiq and streaming), and we need to create config files for each. You can use the code straight from this page, as-is.

# cd /etc/systemd/system/

The first file is called mastodon-web.service and consists of the following:

[Unit]
Description=mastodon-web
After=network.target

[Service]
Type=simple
User=mastodon
WorkingDirectory=/home/mastodon/live
Environment="RAILS_ENV=production"
Environment="PORT=3000"
ExecStart=/home/mastodon/.rbenv/shims/bundle exec puma -C config/puma.rb
TimeoutSec=15
Restart=always

[Install]
WantedBy=multi-user.target

The next file is called mastodon-sidekiq.service and consists of the following:

[Unit]
Description=mastodon-sidekiq
After=network.target

[Service]
Type=simple
User=mastodon
WorkingDirectory=/home/mastodon/live
Environment="RAILS_ENV=production"
Environment="DB_POOL=5"
ExecStart=/home/mastodon/.rbenv/shims/bundle exec sidekiq -c 5 -q default -q mailers -q pull -q push
TimeoutSec=15
Restart=always

[Install]
WantedBy=multi-user.target

The final file is called mastodon-streaming.service and consists of the following:

[Unit]
Description=mastodon-streaming
After=network.target

[Service]
Type=simple
User=mastodon
WorkingDirectory=/home/mastodon/live
Environment="NODE_ENV=production"
Environment="PORT=4000"
ExecStart=/usr/bin/npm run start
TimeoutSec=15
Restart=always

[Install]
WantedBy=multi-user.target

Once all those are saved, we’ve done all we can with the root user for now.

7. Switch to the Mastodon user

If you haven’t yet logged into the server as mastodon, do so now in a second SSH window. We’re going to set up ruby and pull down the actual Mastodon code here.

8. Install rbenv, rbenv-build and Ruby

As the mastodon user, clone the rbenv repo into your home folder:

$ git clone https://github.com/rbenv/rbenv.git ~/.rbenv

When that’s done, link the bin folder to your PATH:

$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile

Then add the init script to your profile:

$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile

That line is valid for the OS we’re on (Ubuntu 16.04 LTS) but it may differ slightly for you. You can run ~/.rbenv/bin/rbenv init to check what line you need to use.

Once you’ve saved that, log out of the mastodon user, then log back in to complete the rest of this section.

Install the ruby-build plugin like so:

$ git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build

Then install Ruby v2.4.1 proper:

$ rbenv install 2.4.1

This could take up to 15 minutes to run!

When it’s done, change to your home folder and clone the Mastodon source:

$ cd ~
$ git clone https://github.com/tootsuite/mastodon.git live
$ cd live

Next up, dependencies! Always more dependencies – we’ll install bundler, then use that to install everything else:

$ gem install bundler
$ bundle install --deployment --without development test
$ yarn install

If all of those succeeded, we’re ready to configure!

9. Configure Mastodon

Before diving into the configuration file, generate 3 secret strings by running this command 3 times:

$ bundle exec rake secret

Copy those out to a text file – you’ll paste them back in later. Create the config file by copying the template, then editing it with nano:

$ cp .env.production.sample .env.production
$ nano .env.production

Inside this file we’re going to make several quick changes.

REDIS_HOST=localhost
DB_HOST=/var/run/postgresql
DB_USER=mastodon
DB_NAME=mastodon_production

To enable federation, you need to set your domain name here:

LOCAL_DOMAIN=example.com

Then, for these 3, paste in each key you generated earlier:

PAPERCLIP_SECRET=
SECRET_KEY_BASE=
OTP_SECRET=

Finally, configure your SMTP details:

SMTP_LOGIN= (whatever your mailgun is)
SMTP_PASSWORD= (whatever your mailgun is)

Save and close the file.

10. Run installer

If you’ve done everything correctly, this command will install the database:

$ RAILS_ENV=production bundle exec rails db:setup

If that passes successfully (it’ll echo out every command it runs), you can then precompile the site assets, which may take a few minutes:

$ RAILS_ENV=production bundle exec rails assets:precompile

At this point, we’re almost ready to go!

11. Configure cronjob

This is technically optional, but highly recommended to keep your instance in good order. As the mastodon user, start by determining where your bundle command lives:

$ which bundle

That path will be substituted for $bundle. Now, edit your own crontab:

$ crontab -e

Select nano (2) if you’re prompted. As of version 1.2 (17 April 2017) you only need one daily task in your crontab:

5 0 * * * RAILS_ENV=production $bundle exec rake mastodon:daily

Save and close the crontab.

12. Log out and return to root

We’re done with the mastodon account. Log out and return to your root shell.

13. Start Mastodon

The moment of truth! Enable the Mastodon services (so that they start on boot):

# systemctl enable /etc/systemd/system/mastodon-*.service

Then fire up Mastodon itself:

# systemctl start mastodon-web.service mastodon-sidekiq.service mastodon-streaming.service

Open up a browser tab on your domain. Mastodon can take up to 30 seconds to warm up, so if you see an error page, don’t fret. Only fret if it’s there for longer than a minute – that requires troubleshooting, which is outside the scope of this document.

You should eventually get a signup page. Congratulations! Register an account for yourself, receive the confirmation email, and activate it. This should enable you (the first user) as an administrator.

14. Securing Mastodon

This is by no means a comprehensive guide to server security, but there are two quick things you can change while the root shell is open. Start by editing the passwd file:

# nano /etc/passwd

Find the mastodon entry (it’ll be near the bottom) and replace /bin/bash with /usr/sbin/nologin. Save and quit. This will prevent anyone from logging in as the mastodon user.

Next, configure ufw. First check if it’s disabled:

# ufw status

It should be off, since this is a brand new VM. Configure it to allow SSH (port 22) and HTTPS (port 443), then turn it on:

# ufw allow 22
# ufw allow 443
# ufw enable
? y

That will prevent any connection attempts on other ports.

15. Enjoy!

If you enjoyed this guide, I’d appreciate a follow! You can find me by searching wogan@wogan.im in your Mastodon web UI. Give me a shout if you were able to get an instance set up with these instructions, or if you ran into any problems.

16. Upgrade to v1.2 (17 April 2017)

If you’ve installed Mastodon according to these instructions, you’ll need to do a few things to upgrade to the latest version.

Start by logging into your instance as the root user, then re-enabling your mastodon user shell (in step 14, change the mastodon user’s shell back to /bin/bash). We’ll use it in a bit to perform the upgrades themselves.

When that’s done, stop the Mastodon services like so:

# systemctl stop mastodon-*

That will shut down all the Mastodon services. In a new window, log into your mastodon user and install Ruby 2.4.1, the new preferred version:

$ cd live
$ rbenv install 2.4.1
$ gem install bundler --no-ri --no-rdoc

This will install the latest Ruby, and the version-appropriate bundler. Now pull down the latest source code:

$ git pull

There are a couple of one-time commands to run – in order, they are to install new dependencies, run database migrations, do a one-time avatar migration, and recompile the frontend.

$ bundle install
$ yarn install
$ RAILS_ENV=production bundle exec rails db:migrate
$ RAILS_ENV=production rake mastodon:maintenance:add_static_avatars
$ RAILS_ENV=production bundle exec rails assets:precompile

When this is all done, make sure your crontab has been updated to use the new mastodon:daily command. Refer to step 11 above for details.

Finally, the teardown – log out of the mastodon user, and switch back to your root connection. Set the mastodon user’s shell back to /usr/sbin/nologin (step 14) and restart the Mastodon services:

# systemctl start mastodon-web.service
# systemctl start mastodon-sidekiq.service
# systemctl start mastodon-streaming.service

Give it a few seconds to warm up, and check that they’re running with:

# systemctl status mastodon-web.service

If you get a green dot with “running”, you’re good to go!

Sources

A lot of this guide was sourced from the official Production guide on the Mastodon Github page. I reorded it into a logical sequence after running through it for a few tries.

This post was updated for v1.2 (and v1.1.2) upgrade notes on 17 April 2017.

Integrating Laravel and Flarum

One of the features I wanted to add to Write500 was a community forum. I’ve worked with Flarum before, and while it’s still ostensibly beta software, it has some really nifty features and works quite well across desktop and mobile. I decided early on that I’d plan to integrate a standalone instance of Flarum into Write500.

Flarum has a JSON API, which as it turns out is really easy to use (despite being insufficiently documented at the moment).

I wanted to set up an integration such that Write500 handled the authentication (you could only log in from the site, not the forum itself), and that changes to a user’s Write500 profile would be synced across.

Laravel Flarum - Page 1 (4).png

This integration broke down into several parts. I’ll go into detail on each one here – hopefully it’s useful!

Contents

  1. Laravel Setup
  2. Flarum Setup
  3. Integration: Creating a new user
  4. Integration: Authenticate the user
  5. Integration: Avatars
  6. Conclusion and Additional Notes

Laravel Setup

At the outset, I implemented a “nickname” field on my User model, with the following constraints (in the validator() method in RegisterController).

'nickname' => 'required|max:32|unique:users'

Those are the same constraints Flarum uses. So now, when I integrate the two (using the nickname as the Flarum username), I can be at least 99% sure it’ll work every time – barring the inevitable weird-unicode-character issue, or some other edge case.

I also ended up adding the following to my User table:

$table->string('nickname');
$table->integer('forum_id')->nullable()->default(null);
$table->string('forum_password')->nullable()->default(null);

Those fields will be populated when the integration runs, and are critical for everything else.

Finally, I needed a few configurable fields. I added these to my config/app.php file:

'forum_url' => env('FORUM_URL', 'https://forum.write500.net'),
'forum_admin_username' => env('FORUM_USER', 'default_admin'),
'forum_admin_password' => env('FORUM_PASS', 'default_pass'),

Note that in the samples below, I don’t reference any of these fields directly.

Flarum Setup

There are a few tweaks made to Flarum to enable all of this. In summary:

  • Installed the latest version of Flarum
  • Set up HTTPS using LetsEncrypt
  • Added the Flarum Links extension
  • Set up the default admin user as a generic account (not named after myself)
  • Disabled new-user registration

About the Links extension

When a user navigates to the community, I want them to be able to navigate back to any other part of the site, without feeling like they’ve really left Write500. To do this, I used the Links extension to create a navbar with the same links as on the main site.

Screen Shot 2017-02-13 at 2.31.49 PM.png

Header on main site

Screen Shot 2017-02-13 at 2.31.58 PM.png

Header on Flarum

I could have used Flarum’s Custom HTML Header feature for that, and created my own menu bar to replace the forum’s one, but that would have lost me the search bar and notification indicator.

About the default admin user

I use the username and password for the admin user to drive the integration. I could technically create an API key on Flarum’s side and just use that instead (it would save me a CURL call), but that also means going directly into the Flarum database to do stuff, which I wasn’t keen on.

One day, when you can generate API keys in the Flarum interface itself, that would obviously be the way to go.

Note about the code samples

The code samples below (all the gist embeds) are not taken directly from my Write500 source – they’ve been adapted for standalone illustration.

Integration: Creating a new User

My first integration requirement:

Whenever a new user is created in Laravel, that same user must be created and activated in Flarum.

The following has to happen:

  1. Authenticate to the Flarum API with my admin account
  2. Create a new user
  3. Activate that user
  4. Link that user to our Laravel user

I ended up putting all of that in a Job, and dispatching it when a new User object was created (in my RegisterController):

dispatch(new \App\Jobs\SyncUserToForum($user));

And now for the actual code!

Step 1: Authenticate

Nothing complicated here – just POSTing to the API to get a token back for this session. This would be the step I could skip if I had a preconfigured API token.

This same method is used whenever I need to log into the Flarum API, so for brevity I’m excluding this code from the rest of the samples.

Step 2: Create a new user

I should, strictly, be checking the API first to see if that nickname or email exists – to prevent any attempts to create duplicate users. However, both Laravel and Flarum itself should fail a new registration if the nickname or email address fields are not unique – so I should be okay there.

Step 3: Activate the user

This could possibly be done in the registration call itself (sending isActivated=true as an attribute upfront), but I’m breaking this out for another reason.

I know that, at some point, I’m going to set up a registration workflow on Write500 that requires users to activate their account before they can log in, and I’m only going to trigger the Flarum activation at that point.

What this means, practically, is that a user’s nickname will be reserved across the system, but will only be @-mentionable once their Write500 account is activated.

Step 4: Link to our Laravel user

When I created this new user, I generated a random password like so, which was used to create the user on Flarum:

$generated_password = substr(md5(microtime()), 0, 12);

The user never has to know this password, since the actual authentication is handled by the site. So we just need to update a few things on our user model:

// $new_user is the result of the POST /api/users call
$user->forum_id = $new_user->data->id;
$user->forum_password = $generated_password;
$user->save();

The forum user ID is not strictly required, but it makes it easier to check whether the user has a forum account yet or not – we don’t want to present the sign-in link to the user before they have a forum account to sign in to:

if($user->forum_id != null) { /* Integrated successfully */ }

Summary

So we now have a job that registers a new user account on Flarum once we have a valid Laravel user – none of it requiring any modifications to Flarum’s internal database.

Integration: Authenticate the user

Next, we need users to be able to authenticate to the forum.

Create a one-click link on Laravel’s side that authenticates and redirects the user to the Flarum instance.

There are a couple of ways to handle this, and I’ll admit upfront that the solution I’ve come up with (as of right now) may not be the best, security-wise.

The following has to happen:

  1. Create a new session token
  2. Set that as a cookie, then redirect to the forum
  3. Update the forum to only allow authenticated users access

Flarum makes this quite easy – the Token you get from the /api/token endpoint can be used as a flarum_remember cookie.

Step 1: Create session token

Once a user has been provisioned in Flarum (they have a numeric forum_id value on their model), we can use their nickname and password to generate a new session token, in the same way we authenticated the admin user.

Step 2: Set token value as cookie

I butted heads with this step for a while, before coming to a solution. On the Flarum side, I had to create a new auth.php file in the forum web root (where index.php and api.php are), with the following:

All that file does is take whatever token value you give it, set it as a cookie, and then bounce you to the forum homepage.

In terms of security, this setup is making me a bit nervous. However, it’s worth noting:

  • The token is sent over HTTPS, and HTTPS URLs and parameters are encrypted
  • The only way to hijack someone else’s account using this, would be knowing their session ID – at which point you can manually add that as a browser cookie anyway
  • The only other exploit that’s possible here is someone trying a script injection attack, but again, that’s nothing you cannot set in your browser manually
  • If a user tries hitting this route manually with a bad token value, they’ll be bounced to the forum, invalidated (see below) and then sent back to the login page.

With that script set up, you can now redirect the user to it from Laravel (as in Step 1), and they’ll be bounced to the forum, fully authenticated.

return redirect()->to('https://my.flarum.url/auth.php?token=' . $session->token);

Step 3: Only permit authenticated users to view the forum

This was another ugly script hack. Flarum itself lets you set permissions so that guests cannot read content – they’ll just see a “no discussions” message when opening the forum.

However, since I’m also planning on removing the Login button on the forum, the only way an unauthenticated user will be able to log into the forum, is by going via my login page.

So I need to add a redirect before Flarum itself fires, to check whether the user is authenticated. I ended up with this:

When a user authenticates with Flarum (which all runs via the api route eventually), a new access token is generated and stored in that table. If the value matches, they’re logged in – if not, they’re bounced to my login page. After logging into Write500, hitting that Community login route will authenticate and take them through.

That could be improved by reading the database connection details from config.php instead, but those values are not likely to change for this setup. I’d also have preferred it if Flarum used some sort of file storage for sessions, in the same way basic Laravel does – but we can’t always get what we want 😉

Integration: Flarum Modification

So we now have the basics down – new users are registered, users can log in via my site, and users cannot reach the forum without being authenticated.

Modify Flarum to integrate the navigation with the main site, and prevent users from logging out via Flarum.

On the Flarum side, I want to change what some of the buttons in the navigation do – for instance, they shouldn’t be able to log out via Flarum (since that still leaves them logged into Write500), and I don’t want them to be able to manage their forum settings directly in Flarum.

After some trial and error, and almost giving up and hacking on the core scripts, I eventually landed on this neat hack.

In the Flarum administration section, under Appearance, you can add your own custom HTML. I used that to deploy a script block that looks like this:

That script is loaded in the page header, and executes immediately. At the end of the page, Flarum loads its app script and creates an app object. Before that happens, there’s nothing to modify, and I cannot force my javascript to run at the end of the page. So instead, my script keeps checking to see if the app variable is available, and when it is, it makes some changes.

With that script in place, the forum is now about as integrated as it can get – there’s just one more thing to cover – Avatars.

Integration: Avatars

Users can upload and crop avatars to Write500.

Automatically synchronize the user’s avatar from Laravel, to Flarum.

I wanted those avatars to sync across to the forum, whenever they were created or updated on Write500’s side. I also have a “Reset to Default” which deletes the current avatar, and that deletion should be synced across as well.

Delete Avatars

This one was pretty easy. On the route that resets my avatar, I dispatch a job that basically does this:

It authenticates to the Write500 API, and issues a delete request against the avatar resource. Simple.

Upload avatars

This one’s a bit more complicated. The API endpoint is expecting a form-data upload, which takes a bit of fiddling to get right on PHP’s end.

After a user uploads, crops and saves an avatar, it’s stored in the public/ directory with a GUID, which is saved against the User model. All I really need to do is read that file from disk and upload it to the Flarum API. Which I achieve like so:

That bit of code will blindly upload a new avatar. Flarum doesn’t require you to delete an existing avatar before uploading a new one, so I can trigger this all I want. Right now, it’s just dispatching as a job after a new avatar is saved in Write500.

I’m not sure whether or not the User-Agent is strictly required for this, but I haven’t had time to go back and test it. I do know that the Flarum API does behave differently depending on whether it thinks it’s being hit from a browser.

Conclusion

So that’s an exhaustive look at how I’ve enabled the following integrations:

Laravel Flarum - Page 1 (5).png

I’m sure that some elements of this could be improved, and if I make any substantial updates to this, I’ll likely write a follow-up post here.

Additional Considerations

This is all still a work in progress, and I’m sure I’ll find more bugs as I go along.

Right now, for instance, I think it might make more sense to keep the user token cached in the User model, and only issue a new one when the current one expires. Right now, if you upload an avatar (for instance), it generates a new token that might invalidate the old one. So you’d have to log into the community from scratch to get access again.

There’s also notification integration. There’s an API endpoint that returns information about new notifications (@-mentions, etc), which I would want to display in the Write500 navbar.

I’d also want to take over the profile view eventually. I’m aiming to build a user profile page in Write500 to show some high-level stats, and I’d rather integrate forum activity in there, as opposed to having Flarum display any user profile page at all. Finally, I need to consider what will happen when users cancel their Write500 subscriptions.

Those are all questions for another day though!

DigitalOcean vs AWS Lightsail

I’ve been a big fan of DigitalOcean pretty much since they launched. Their pricing was cheap and simple, and their service was a joy to use. What made it different (as compared to other VPS hosts of the time) was the sheer simplicity of setup. The first time I used it, I was up and running with an SSD-backed VPS in under a minute – and blown away, of course.

In 2016, Amazon launched Lightsail – presumably in an attempt to tap into the market for developers who need quick and cheap VPSes. It got me thinking whether or not it’d be worth it to actually run some of my VMs there. At the lower tiers, at least, it looked like Lightsail had a cheaper offering.

A word on features

Each VPS host offers the same thing, fundamentally: CPU, RAM, SSD storage and bandwidth. They do diverge on the added-value features – for instance, DigitalOcean includes free DNS and monitoring, whereas Amazon expects you to pay for Route53 and CloudWatch respectively.

In this case though, I’m looking purely at the cost of the servers themselves.

The Basics

For this pricing comparison, I’m first looking at the per-hour cost – since that’s what you get billed on.

2017-02-11 02_33_07-VPS.xlsx - Excel.png

  • 0.5GB RAM: Out of the gate, Lightsail and DigitalOcean have the same pricing and features for their smallest instance.
  • 1GB RAM: One tier up, Lightsail actually works out fractionally cheaper for the features offered by DigitalOcean. But that lead doesn’t last.
  • 2GB RAM: In this category, Lightsail is slightly cheaper, but DigitalOcean offers double the CPU capacity at a similar price point.
  • 4GB RAM: There’s parity again here with a comparative saving of just under 10%, if you were to select Lightsail over DigitalOcean.
  • 8GB RAM: And at this point, Lightsail is slightly cheaper (<10%), but again, DigitalOcean offers double the CPU.

This is how it works out on a Monthly basis:

2017-02-11 02_33_22-VPS.xlsx - Excel.png

Interestingly, despite having higher per-hour costs than some Lightsail options, DigitalOcean’s advertised monthly cost is lower. They must be using a strange definition of “Monthly” in calculating that, so to make it fair, I’m basing this on a 744-hour (31-day solid) month. That’s the upper bound for what you’d need to budget for.

My conclusion: at these instance sizes, DigitalOcean is better value for money across the board. With the possible exception of the 4GB RAM instance offered by LightSail – there you’d save roughly 10% over DigitalOcean.

Let’s talk about Transfer

In the next section, I’m going to compare these prices to raw AWS EC2 prices. One of the stated benefits of Lightsail is that once you outgrow your initial servers, you can migrate and extend by leveraging the AWS cloud. Which sounds nice in theory.

DigitalOcean (and Lightsail, by the looks of it) bundle a bandwidth allocation in at each price point.On DigitalOcean, that Transfer number counts for incoming and outgoing traffic on the public network interface (meaning that transfer on a private network is free).

AWS EC2 has a different approach. Most transfer into EC2 is free, and transfer out (uploading from your VPS to somewhere else) is charged differently depending on the destination. If it’s to another internal AWS service you usually get a much cheaper rate, as compared to transfer to the Internet.

While DigitalOcean and Lightsail both make huge bandwidth allocations available, the assumption (on their end) is that most users won’t actually use all of that bandwidth. If users did actually manage to max it out every month, the pricing would be very different.

Comparing to EC2

So let’s look at what it would cost to get the same features and bandwidth allocation directly from Amazon EC2. In this comparison, I’m basing everything off the N. Virginia region (their largest, oldest and cheapest), and I’m assuming On-Demand pricing for Linux VMs. I’ll compare it against Lightsail, which is only marginally more expensive than DigitalOcean to begin with.

2017-02-11 02_30_52-VPS.xlsx - Excel.png

Say what? Must be a calculation error, right?

EC2 charges for each component separately, and in excruciating detail. You’ll rent a Compute instance for RAM and CPU, then attach an Elastic Block Store volume to serve as the storage, and you’ll pay separately for the bandwidth. Complicated? You bet!

So in that table, the cost of each component breaks down like so:

2017-02-11 02_31_43-VPS.xlsx - Excel.png

Here’s where that bundled transfer stuff comes into play. If you look at just the Instance and Storage costs, it’s about on-par with Lightsail. The moment you want to serve traffic to the Internet, though, you’re paying $0.09/GB – and budgeting to be able to do terabytes worth of transfer every month is really expensive.

(Incidentally, pushing everything to AWS CloudFront won’t save you, since they start at $0.085/GB for transit to the Internet).

In truth, the bundled transfer included by DigitalOcean and Lightsail is what makes the difference.

Conclusions

If you were already on DigitalOcean, you’re probably congratulating yourself right now for making the smarter choice. And you’d be right.

If you’re on Lightsail, there’s no real reason to move. But if you’re running a couple of smallish EC2 VMs, and are sweating the bandwidth costs every month, it might be worth switching and taking advantage of the free bundled bandwidth.

Write500 – One Month In

I promised myself I’d write a post at the end of January, reflecting on the progress and lessons learned from Write500 so far. It’s been a busier month than I was expecting!

My goals

At the start of the month I set a few goals for myself:

  • I’d write to the same prompts that users get every day
  • I’d broadcast 1 image per day on social media to promote Write500
  • I’d work towards a web app that could go into beta by the end of the month

As it turns out, doing all of that with consistency, on top of having a regular 9-5 is a bit more of a challenge than I initially thought!

Some numbers

Before I get into the full run-down, here’s the part most people are likely to care about: the stats!

I started Write500 as a free mailing list, launched on 1 January of this year, and promoted in a few places (notably, Instagram). All the campaigns have now expired, and as we approach the end of the month, here are the highlights for that list’s performance:

Screen Shot 2017-01-28 at 3.57.28 PM.png

Of the 709 users currently subscribed to the free daily prompt list:

  • 683 (96.2%) are still active, and receive daily emails
  • 26 (3.7%) have opted out
  • 144 (20.3%) open their prompt emails every day

Since migrating the user database to the new list system on 17 Jan (more details on that below), I’ve had an additional 23 users join the list – without spending any money, and barely any effort, on marketing.

Screen Shot 2017-01-28 at 4.19.54 PM.png

My traffic curve looks exactly like you’d expect – lots of interest during the time the Instagram campaign was running, and very little when it ended. Interestingly, the campaign only officially concluded in the early hours of 13 January, after interest to the site started slowing down.

Filtering for all traffic starting 14 January, this is what my Acquisition Overview looks like:

Screen Shot 2017-01-28 at 4.22.08 PM.png

It’s encouraging to see Referral and Organic Search showing up in Channels. I’ve still got a ton of work to do, in terms of content marketing – a full site+blog overhaul, to start.

Finally, and probably my favorite part:

Screen Shot 2017-01-28 at 4.24.42 PM.png

Write500 is now ranking #1 for its own keyword on Google, Bing and Yahoo (hence DuckDuckGo putting it at #1 too). Which, to be fair, wasn’t much of a challenge – there is only 1 other domain (write500.com) that could be said to be competing for that keyword.

I do feel that this bodes well for offline and word-of-mouth marketing, though!

Actual achievements

Goal #1: Writing

In terms of the writing, I started out pretty strong – I produced 12249 words between 1 Jan and 12 Jan. There were a few blog posts in there, but most of the words came from the daily prompts.

I started losing traction on this around 13 January, which is no coincidence – that’s when my mental bandwidth for this project started to narrow, with dayjob-related work ramping up. I had to start making compromises, and figured that it would be better to prioritize the app itself.

I’m still pretty happy with what I achieved here, though – 12k words in January represents more than I wrote across most of 2016. And I now know I’m capable of this, I just need to find the mental space again.

Goal #2: Image Publishing

When I started working on the prompt database for Write500, my first step was to collect 366 inspirational, motivational and literary quotes. They were originally intended to be used to decorate the daily emails, providing something else to read other than the prompt and the instructions itself.

When I spun up the Write500 Instagram account, I realized I could find a secondary use for that content – publishing each quote as an image. So I created a batch of 25 images – here’s a sample:

I used one of those as the creative on my Instagram ad, and it performed really well, so I decided to keep publishing those images every day. Even after the campaign ended, those images kept getting likes (as much as 80 per image).

However, this too suffered from a lack of time. I published every image on schedule, from 1 Jan to 18 Jan, until I missed my first day. I then managed to publish daily until the 21st, at which point I hit a bit of a wall.

I needed to make time to create more images in order to continue publishing daily, but at this point there wasn’t an immediate return on it. Even though the images are well-received on Instagram, I got very few signups that way. The only benefit to continuing might have been to keep building up my profile, which felt a bit like putting the cart before the horse: There’s still no app I can send people to.

I will eventually pick this up again, and produce the remaining 340-or-so images. That’ll be a marketing exercise though, and right now, the app is more important.

Goal #3: A Write500 Beta

I might actually make this one! Right now, it’s Saturday afternoon. I’m taking a break from a few hours of dev to write this post, and I’m intending to continue when it’s published. There’s only one core feature left to add, a few style cleanups, and then I’ll be able to start inviting beta users.

I had to jump through quite a few hoops for this one. When I started Write500, my plan was to build one app that allowed both free and paid signups, and I’d port over the users from the free mailing list into it.

When I actually tried building that out though, I started running into issues. Small ones, sure, but I could already see the technical debt forming – I’d have to make lots of exclusions and compromises to have both personas co-exist in one database.

So instead, I took a few days to break out the free mailing list feature into its own project. It now lives at https://list.write500.net as a standalone automated mailer, and exists independently from the main project.

I might even polish that up and release it as a standalone product someday. It just needs a nicer content editor and subscriber management features, but is otherwise a fairly capable bulk mailer (including a bespoke interaction tracker), using the very-reasonably-priced Amazon SES as a backend. Who can argue with $0.10 per thousand mails, and not having to send your subscriber or interaction data to a third party?

With the List system out of the way, I started building the app itself. I didn’t have much of an architecture or feature layout in mind when I started – I just knew the broad strokes of what I wanted to accomplish. Which, to date, has been the following:

  • Subscription integration with Paypal (the only global payment provider that I can use right now), using their PHP SDK to create and execute Billing Agreements.
  • An MVP for the main interaction loop (receive prompt, write words, get statistics), in a basic but functional Bootstrap-based UI
  • A Programs feature, that delivers the prompts on a schedule – this replaces the mailing list in many ways.

And I do have a few screenshots to share! Click to enlarge.

Up Next?

So right now, Write500 addresses the write-in-isolation use case: the writers who prefer to work alone, and only need to see their statistics and streaks building up over time for motivation.

Of course, there are other types of writers out there. I’m in a slightly different cohort myself: I prefer writing as part of a small group, sharing notes and progress as I go. I find that being surrounded by other writers helps the motivation somewhat.

Logically, Write500 needs a Groups feature of some kind, but I’ve given that far more thought, and I’m going to try something kinda new: Tribes.

About Tribes

One of the problems I have with normal groups (the sort you have on Facebook, for instance), is that as the size of the group goes up, the quality of the interactions goes down.

Not all interactions, of course – users still post useful things, and engage in useful ways. But when a group gets too big, it loses its initial sense of closeness and community. More members eventually means more rules, more moderation, and inevitably, users going quiet, leaving the group, or splintering off to form their own.

For Write500, I need a Groups feature that encourages everyone in the group to engage on a regular (daily, preferably) basis, while still feeling that they’re getting good personal engagement with other users. Doing this in a classic open-ended group would be difficult. That sort of interaction would be deafening, for one: signal-to-noise will be going way down.

And there’s also the fact that different personas want different things out of their groups. Some prefer lively debate, some prefer terse updates, some prefer checking in multiple times per day, some prefer checking in once a week.

So with Tribes, I’m going to create a groups feature that has the following characteristics:

  • Max of 10 users per Tribe (to start), and Tribes need to split in order to grow.
  • Users join on a time-limited tryout (or need to be invited), and every other member of the Tribe has to explicitly (and anonymously) vote to include them permanently
  • Notifications and events from Tribes will have a dedicated section in Write500 (Tribe Newsfeed), and be the most visible form of notification available
  • Users can only belong to one Tribe at a time

Completely antithetical to standard community growth tactics? You bet!

With Tribes, I specifically want groups of writers to form strong relationships with other writers in their genre/pace/orbit, and feel at ease about sharing more about their work than they’d regularly share on an open group.

I also want to make sure that being in a Tribe is a rewarding experience, and that other members of the Tribe pay attention to what you have to say – not always a given in a group that can grow uncontrollably.

When this comes to the actual writing, there’d be integration there too – the ability to broadcast a completed prompt to your Tribe, letting them review and comment on your work. Which feels like a better solution to me, than just broadcasting your work in a public square and hoping to catch people’s attention.

Of course, this entire experiment could fail and I just end up going back to standard groups! I’m optimistic, though, that a format like this would create an environment that some users would find useful.

Shoutout: StartupStudyGroup

If you read this far down, well done! You’re clearly someone who’s interested in the details, and how new products and services come about -so you really should check out startupstudygroup.com!

I joined the SSG Slack group earlier this year, and the community there has provided me with valuable insights and encouragement so far. If you join the SSG Slack, come say hello in #write500!

And now, back to the grindstone.