write.as dev

from the engineering team

Last night went well. I looked at Android v2 for the first time in a while and audibly said wow when I first opened it, because it looks so good. The biggest thing is the color scheme change, but it looks really good.

I updated the app icon too, and I'm pretty happy with it. I think it looks much more modern. I also chipped away at issues with saving edited posts and extracting or displaying post titles in the UI. The code is a mess as I've bolted the new richer post structure onto the old one, using SQLite no less where the old one used JSON and user preferences — and they're living side-by-side in v2. But it'll pass for now so I can get this out the door and then take my time migrating everything to the new storage system.

I ended the night by adding support for Markdown on anonymous posts. I've kept an eye on bold.io since it launched just after we were on Product Hunt last November, and lately I've noticed people asking if there's any new development on it on Twitter. @bold hasn't tweeted since December, and the creators seemed to have moved on, based on their bios. So I've been reaching out to people publicly tweeting about it, and mentioning that Write.as is here, and isn't just a cute side-project. Bold supports Markdown out of the box (like Telegra.ph), so I wanted some way for users to get a similar experience on Write.as.


I've also decided to start writing here more — ideally, every day I work on Write.as. I'll try to chronicle the important things that could be relevant to anyone building a product by themselves.


Today there was an influx of new anonymous users. Last week a professor in Saudi Arabia tweeted out a link to Write.as and we saw a huge influx of new users (I was especially excited for that because of all work I'd already done to support RTL languages back in the day). Today someone else prominent must've retweeted it, because another huge rush came in, and altogether we saw 235 new posts today (the daily average for the past few months has been about 100/day).

Last year around this time I saw our second or third paying customer sign up, and was telling people about how the Android app was coming by the end of July. That turned to August, then October, then “soon.”

Tonight, I'm putting the blinders on to all other concerns (including marketing, minor customer requests, UX improvements, SEO, some guy that talked about acquiring Write.as today, everything).

I will tackle this.

I finally took a little time to make some executive decisions on the mobile UI and pushed out fixes to the pad on mobile.

(And I'm testing it out now!)

You should also be able to add Write.as to your home page on iOS. Android support for the same, as well as offline support, is up next.

Today an account named ghostrecon was created. I checked our analytics to see that they were from Ukraine, like the rest of our recent spam posts.

I queried our API to see what posts they'd created while I watched the application logs, but they hadn't published anything, instead only updating their blog. I waited, until I saw they changed their blog description to:

Download Key Generator – [URL redacted] – Tom Clancys Ghost Recon Wildlands (PC) Key Free, Steam Key Generator download. Ghost Recon Wildlands CD Key.

Very clever.

As soon as I saw this, I saw Google had already crawled the site. Fuck.

So I banned them, and tonight will have to run blog descriptions through our flagging system, too.

The past few days have been spent on building hellbanning and moderation systems to handle an influx of posts offering torrents, cracks, keygens, and patches for certain PC games. I first saw a lot of traffic going to these posts. Most were coming from Google, so I quickly built an internal blacklist of posts that would have noindex on them — simple, quiet banning in the spirit of a light touch.

Then a URL got taken down from Google search results from a DMCA complaint. Then someone said they got a virus from one of the posts on reddit.

When the virus happened, I had had enough, and took the more nuclear route. The “noindex” list also supported outright hellbanning posts. So users can still edit and maintain their posts (no information is lost), but readers won't see them. I did the same for blogs that were pushing this type of content, too.

The person pushing this content has been fairly persistent, so our spam filtering has gotten more robust to handle their various maneuvering tactics, so they actually get blocked from posting certain content and see that we're watching them. But I also implemented a simple flagging mechanism so I get notified of suspicious posts and can noindex or hellban them in one click, no matter where I am.

The hope, of course, is that eventually these fuckers go away. I've had to spend a lot of time hastily implementing these measures and bogging down the application with them. But on the bright side, it is a baby step into the community moderation we're going to need in the near future. It's spurred the creation of our community guidelines and is helping us establish a no abuse policy now, instead of when it gets bad, or starts happening between humans.


Funnily enough, this post triggered the spam filter I built. If I didn't run the site and have blocked posts go to my inbox, that sinking feeling of losing all your writing would've lasted much longer!

As a young product (especially an anonymous one), you have to protect the environment you want to support on your service from early on. Ideally you want people to use your creation for good, magical things you could've never thought of. While that seems to be the overwhelming case on Write.as, there are of course people who try to take advantage of a service like this.

One of the earliest cases was last summer, when I noticed an unusually high number of new posts start to come in while passively watching our scrubbed application logs. They happened regularly and frequently enough to seem like some kind of robotic use, so I went into our web server logs, and saw they all originated from the same IP address. Since I assumed some kind of spammy behavior, I then went and looked at a few of the recently-published posts. Each was the exact same, with a few words and a link to some site. I saw most had a few views, with a lot of referrals from facebook. I blocked the IP address from publishing new posts, verified the posts were stopping, and went back to bed.

The next night I saw the same thing, this time from a different IP address. The text was the same and the URL was now a different, shortened one. So I modified the application to check for similar text and, if it was found, prevent publishing and instead present the spammer with an amusing “spam” page asking the poster why they aren't writing about their feelings. They eventually stopped trying.

I've spent the last few months saying “I'm building a sign up page.” This is basically the “New Blog” dialog in the writer/pad, except with a payment form included as well, located at /new/blog. The idea is to get more people converting to paid accounts, especially since many current customers upgraded within 0-1 days of using Write.as.

I spent a lot of time worrying about the whole thing requiring two API calls to work (one for sign up, one for payment); I really don't like the potential for the client to be in that in-between state with a bad internet connection, or some unforeseen JS error. But the registration system is already really robust, so worst-case, users will just have to upgrade the old fashioned way.

Though there were other things going on, this took a lot longer than I thought, and amazingly, I'd built things right enough the first time through to make adding this easy.

Now everyone can change their username if they want. I delayed implementing this because it needed to redirect old URLs to the new blog address, and I thought it'd be more difficult. It was actually really easy.

The schema:

| prev_alias | new_alias |

The logic upon updating a name:

# Remove any existing redirects with the new blog name
DELETE FROM collectionredirects WHERE prev_alias = 'blog2'
# Redirect all redirects to the previous name to the new one
UPDATE collectionredirects SET new_alias = 'blog2' WHERE new_alias = 'blog1'
# Redirect previous name to the new one
INSERT INTO collectionredirects (prev_alias, new_alias) VALUES ('blog1', 'blog2')

Then the application simply had to look things up and send the right HTTP responses. Sweet! A web that doesn't break when you change some names.

The problem now is notifying other browser sessions and (in the future) other clients about username changes. There's no real straightforward way to do this with adding extra backend bloat, so I'm leaving this alone for now until the issue arises.