-
Dropping SaaS
byThe mantra in bootstrapping circles for the past while has been βcharge moreβ. And the best way to charge more, over time, is a SaaS. So itβs natural that most bootstrapers default to a SaaS pricing model when starting their new projects and companies.
Iβm no different. I build web-apps professionally and have for the past 10 years. Web apps are my bread and butter.
But when I compare my successful SaaS projects to my successful desktop app projects, no matter the metric, Iβve always made more when I charge less and charge it once.
And since Iβve been so focused on SaaS and this charge more mentality, Iβve automatically dismissed ideas that I had that werenβt SaaS.
After attempting to build a number of web apps independently Iβve mostly stopped midway through. The slog of getting the basics perfect, managing servers, dealing with recurring payments, itβs too much like my day-job.
And so I find myself considering going back to my old bread and butter for side-projects: native apps for the Macintosh.
So far Iβve got a few ideas for small utility apps. The ones Iβm most interested in are the ones that fit in the open web and apps that can help increase privacy for its users.
Itβs been a breath of fresh air and Iβm excited to be having fun making things again.
-
Checkin to St. Marc CafΓ© (γ΅γ³γγ«γ―γ«γγ§ ε±±ζε°εΊ)
Fika time. εΊγ£οΌ First coffee in a cafe since February-ish.
-
Checkin to MOS Cafe (γ’γΉγ«γγ§)
Espresso burger
-
Checkin to Kugenuma Beach (ι΅ ζ²Όζ΅·ε²Έ)
Social distancing at the beach.
-
How to fix HTTP_HOST Errors with Django, Nginx, and Let's Encrypt
byDjango has a nice security feature that verifies the request HOST header against the ALLOWED_HOSTS whitelist and will return errors if the requesting host is not in the list. Often youβll see this when first setting up an app where you only expect requests to
app.example.combut some bot makes a request to<server ip address>.While itβs not strictly harmful to add your server ip to your ALLOWED_HOSTS, in theory, it does allow bots to easily reach and fire requests to your Django app, which will needlessly consume resources on your app server. Itβs better to filter out the requests before they get to your app server.
For HTTP requests, you can block requests by adding default_server that acts as a catchall. Your app server proxy then set its server_name to the a domain in your ALLOWED_HOSTS. This simple configuration will prevent
http://<server ip address>requests from ever reaching your app server.
// default.conf
server {
listen 80 default_server;
return 444;
}// app.conf
upstream app_server {
server 127.0.0.1:8000 fail_timeout=0;
}server {
listen 80; server_name {{ WEB_SERVER_NAME }};
access_log /var/log/nginx/access.log access_json;
error_log /var/log/nginx/error.log warn;location /static/ {
alias /var/app/static/;
}location / {
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-Request-Id $request_id;
proxy_redirect off; proxy_pass http://app_server;
}
}However, once you enable SSL with Letβs Encrypt, despite the fact that they matching by host, as there is only one SSL server configuration by default, it routes all https traffic to the same host. What this means is thatΒ while requests made to
http://<server ip address>will continue to be blocked, requests tohttps://<server ip address>will begin to be forwarded to your django app server, resulting in errors. Yikes!The solution is to add a default SSL enabled server, much like your http configuration. Thee only tricky bit is that all ssl configurations must have a valid ssl certificate configuration as well. Β Rather than making a self-signed certificate I reused my letβs encrypt ssl configuration.
// default.conf
server {
listen 80 default_server; return 444;
}server {
listen 443 ssl default_server;
ssl_certificate /etc/letsencrypt/live/{{ WEB_SERVER_NAME }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ WEB_SERVER_NAME }}/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;if ($host != {{ WEB_SERVER_NAME }}) {
return 444;
}
}By adding a default SSL server to your nginx config your
server_namesettings will be respected and requests that do not match your host name will no longer be forwarded to your app server. -
Checkin to Starbucks
by in Kanagawa, Japan -
How I Architect My Graphene-Django Projects
byRecently at work Iβve been working quite a bit with Django and GraphQL. There doesnβt seem to be much written about best practices for organizing your Graphene-Django projects, so Iβve decided to document whatβs working for me. In this example I have 3 django apps: common, foo, and hoge.
Thereβs two main goals for this architecture:
- Minimize importing from βoutsideβ apps.
- Keep testing simple.
Queries and Mutations Package
Anything beyond simple queries (i.e. a query that just returns all records of a given model) are implemented in their own file in the queries or mutations sub-package. Each file is as self-contained as possible and contains any type definitions specific to that query, forms for validation, and an object that can be imported by the app's
schema.py.Input Validation
All input validation is performed by a classic Django form instance. For ease of use django form input does not necessarily match the GraphQL input. Consider a mutation that sends a list of dictionaries with an object id.
{
"foos": [
{
"id": 1,
"name": "Bumble"
},
{
"id": 2,
"name": "Bee"
]
}Before processing the request, you want to validate that the ids passed actually exist and or reference-able by the user making the request. Writing a django form field to handle input would be time consuming and potentially error prone. Instead each form has a class method called
convert_graphql_input_to_form_inputwhich takes the mutation input object and returns a dictionary that can be passed the form to clean and validate it.from django import forms
from foo import modelsclass UpdateFooForm(forms.Form):
foos = forms.ModelMultipleChoiceField(queryset=models.Foo.objects)@classmethod
def convert_graphql_input_to_form_input(cls, graphql_input: UpdateFooInput):
return { "foos": [foo["id"] for foo in graphql_input.foos]] }Extra Processing
Extra processing before save is handled by the form in a
prepare_datamethod. The role this method plays is to prepare any data prior to / without saving. Usually I'd prepare model instances, set values on existing instances and so forth. This allows thesave()method to usebulk_create()andbulk_update()easily to keeps save doing just that - saving.Objects/List of objects that are going to be saved / bulk_created / updated in save are stored on the form. The list is defined / set in init with full typehints. Example:
from typing import List, Optionalclass UpdateFooForm(forms.Form):
foos = forms.ModelMultipleChoiceField(queryset=models.Foo.objects)def __init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self.foo_bars: List[FooBar] = []
self.bar: Optional[Bar] = NoneType Definition Graduation
Types are defined in each query / mutation where possible. As schema grows and multiple queries/mutations or other app's queries/mutations reference the same type, the location where the type is defined changes. This is partially for a cleaner architecture, but also to avoid import errors.
βββ apps
βββ common
β βββ schema.py
β βββ types.py # global types used by multiple apps are defined here
βββ hoge
βββ mutations
β βββ create_hoge.py # types only used by create_hoge are in here
β βββ update_hoge.py
βββ queries
β βββ complex_query.py
βββ schema.py
βββ types.py # types used by either create/update_hoge and or complex_query are defined hereExample Mutation
The logic kept inside a query/mutation is as minimal as possible. This is as it's difficult to test logic inside the mutation without writing a full-blown end-to-end test.
from graphene_django.types import ErrorTypeclass UpdateHogeReturnType(graphene.Union):
class Meta:
types = (HogeType, ErrorType)class UpdateHogeMutationType(graphene.Mutation):
class Meta:
output = graphene.NonNull(UpdateHogeReturnType)class Arguments:
update_hoge_input = UpdateHogeInputType()@staticmethod
def mutate(root, info, update_hoge_input: UpdateHogeInputType) -> str:
data = UpdateHogeForm.convert_mutation_input_to_form_input(update_hoge_)
form = MutationValidationForm(data=data)
if form.is_valid():
form.prepare_data()
return form.save()
errors = ErrorType.from_errors(form)
return ErrorType(errors=errors)Adding Queries/Mutations to your Schema
This architecture tries to consistently follow the graphene standard for defining schema. i.e. when defining your schema you create a
class Queryandclass Mutation, then pass those to your schemaschema = Schema(query=Query, mutation=Mutation)Each app should build its Query and Mutation objects. These will then be imported in the schema.py, combined into a new Query class, and passed to schema.
# hoge/mutations/update_hoge.pyclass UpdateHogeMutation:
update_hoge = UpdateHogeMutationType.Field()
# hoge/mutations/schema.py
from .mutations import update_hoge, create_hoge
class Mutation(update_hoge.Mutation,
create_hoge.Mutation):
pass# common/schema.py
import graphene
import foo.schema
import hoge.schemaclass Query(hoge.schema.Query, foo.schema.Query, graphene.GrapheneObjectType):
passclass Mutation(hoge.schema.Mutation, foo.schema.Mutation, graphene.GrapheneObjectType):
passschema = graphene.Schema(query=Query, mutation=Mutation)
Directory Tree Overview
βββ apps
βββ common
β βββ schema.py
β βββ types.py
βββ foo
β βββ mutations
β β βββ create_or_update_foo.py
β βββ queries
β β βββ complex_foo_query.py
β βββ schema.py
βββ hoge
βββ mutations
β βββ common.py
β βββ create_hoge.py
β βββ update_hoge.py
βββ queries
β βββ complex_query.py
βββ schema.py
βββ types.py -
by
Went for a drive today (at Leoβs insistence) down to Chigasaki-Shi. Felt a little bad with Yokohama plates in Shonan plate territory.
Didnβt get out of the car, but man itβs so green and nice out there. Saw some huge koinobori too.
-
A Glimpse of the Future
byOne of the common memes to come from covid19 is to post a before-after photo of a famous city or landmark. The before covid19 photo is the city as weβve become accustomed to it: brown air full of smog. The after covid19 at the same location, but with naturally blue skies and clear air.
With everyone social distancing and automobile/truck traffic near zero we have been given a rare opportunity.Β We no longer have to imagine what our air and cities could be like if we didnβt drive pollution emitting vehicles everywhere, we can see, taste, and smell it with our own eyes.
Air pollution from cars and trucks have been suffocating our cities slowly, like one boilβs a frog, so we acclimate and brown air becomes βnormalβ and the way things have always been. With the burner temporary malfunctioning we can see just what a precarious position weβve put ourselves in.
When this is all done and our lungs have acclimated to clean air weβll have a choice: do we go back to the way things were and forget what weβve experienced, or do we the courage to demand a change.
https://twitter.com/sistercelluloid/status/1249027255797460993
-
UNIX: Making Computers Easier To Use
byWatching videos like this one about UNIX system from 1982 is a great reminder that no matter what you're building today, we all stand on the shoulders of giants. Highly worth 20 minutes of your time.
https://youtu.be/XvDZLjaCJuw