Hello World!

Hi, I'm Joel Kuijper, a fullstack developer from the Netherlands. I'm currently working at De Indruk as a Full-Stack Developer.

This website is a place for me to write about stuff I make and learn about. If you are curious about the tools and apps I use, you can find a list of my favorites here.

Find me on

Posts

Notes

  • Nixpacks configration for deploying Laravel on Railway

    This is the Nixpacks configuration file that I use to deploy Laravel apps on Railway.

    [phases.setup]
    nixPkgs = ["...", "python311Packages.supervisor"]
    
    [phases.build]
    cmds = [
        "mkdir -p /etc/supervisor/conf.d/",
        "cp /assets/worker-*.conf /etc/supervisor/conf.d/",
        "cp /assets/supervisord.conf /etc/supervisord.conf",
        "chmod +x /assets/start.sh",
        "..."
    ]
    
    [start]
    cmd = 'bash /assets/start.sh'
    
    [staticAssets]
    "start.sh" = '''
    #!/bin/bash
    
    # Transform the nginx configuration
    node /assets/scripts/prestart.mjs /assets/nginx.template.conf /etc/nginx.conf
    
    # Start supervisor
    supervisord -c /assets/supervisord.conf -n
    '''
    
    
    "supervisord.conf" = '''
    [unix_http_server]
    file=/assets/supervisor.sock
    
    [supervisord]
    logfile=/var/log/supervisord.log
    logfile_maxbytes=50MB
    logfile_backups=10
    loglevel=info
    pidfile=/assets/supervisord.pid
    nodaemon=false
    silent=false
    minfds=1024
    minprocs=200
    user=root
    
    [rpcinterface:supervisor]
    supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
    
    [supervisorctl]
    serverurl=unix:///assets/supervisor.sock
    
    [include]
    files = /assets/worker-*.conf
    '''
    
    "worker-nginx.conf" = '''
    [program:worker-nginx]
    process_name=%(program_name)s_%(process_num)02d
    command=nginx -c /etc/nginx.conf
    autostart=true
    autorestart=true
    stdout_logfile=/var/log/worker-nginx.log
    stderr_logfile=/var/log/worker-nginx.log
    '''
    
    "worker-phpfpm.conf" = '''
    [program:worker-phpfpm]
    process_name=%(program_name)s_%(process_num)02d
    command=php-fpm -y /assets/php-fpm.conf -F
    autostart=true
    autorestart=true
    stdout_logfile=/var/log/worker-phpfpm.log
    stderr_logfile=/var/log/worker-phpfpm.log
    '''
    
    "worker-laravel.conf" = '''
    [program:worker-laravel]
    process_name=%(program_name)s_%(process_num)02d
    command=bash -c 'exec php /app/artisan queue:work --sleep=3 --tries=3 --max-time=3600'
    autostart=true
    autorestart=true
    stopasgroup=true
    killasgroup=true
    numprocs=2
    startsecs=0
    stopwaitsecs=3600
    stdout_logfile=/var/log/worker-laravel.log
    stderr_logfile=/var/log/worker-laravel.log
    '''
    
    "worker-inertia-ssr.conf" = '''
    [program:inertia-ssr]
    process_name=%(program_name)s_%(process_num)02d
    command=bash -c 'exec php /app/artisan inertia:start-ssr'
    autostart=true
    autorestart=true
    stderr_logfile=/var/log/worker-inertia-ssr.log
    stdout_logfile=/var/log/worker-inertia-ssr.log
    '''
    
    "worker-nightwatch.conf" = '''
    [program:nightwatch]
    process_name=%(program_name)s_%(process_num)02d
    command=bash -c 'exec php /app/artisan nightwatch:agent'
    autostart=true
    autorestart=true
    stdout_logfile=/var/log/worker-nightwatch.log
    stderr_logfile=/var/log/worker-nightwatch.log
    '''
    
    "php-fpm.conf" = '''
    [www]
    listen = 127.0.0.1:9000
    user = www-data
    group = www-data
    listen.owner = www-data
    listen.group = www-data
    pm = dynamic
    pm.max_children = 20
    pm.min_spare_servers = 2
    pm.max_spare_servers = 8
    pm.start_servers = 4
    clear_env = no
    php_admin_value[post_max_size] = 35M
    php_admin_value[upload_max_filesize] = 30M
    '''
    
    "nginx.template.conf" = '''
    user www-data www-data;
    worker_processes 5;
    daemon off;
    
    worker_rlimit_nofile 8192;
    
    events {
      worker_connections  4096;  # Default: 1024
    }
    
    http {
        include    $!{nginx}/conf/mime.types;
        index    index.html index.htm index.php;
    
        default_type application/octet-stream;
        log_format   main '$remote_addr - $remote_user [$time_local]  $status '
            '"$request" $body_bytes_sent "$http_referer" '
            '"$http_user_agent" "$http_x_forwarded_for"';
        access_log /var/log/nginx-access.log;
        error_log /var/log/nginx-error.log;
        sendfile     on;
        tcp_nopush   on;
        server_names_hash_bucket_size 128; # this seems to be required for some vhosts
    
        server {
            listen ${PORT};
            listen [::]:${PORT};
            server_name localhost;
    
            $if(NIXPACKS_PHP_ROOT_DIR) (
                root ${NIXPACKS_PHP_ROOT_DIR};
            ) else (
                root /app;
            )
    
            add_header X-Content-Type-Options "nosniff";
    
            client_max_body_size 35M;
    
            index index.php;
    
            charset utf-8;
    
    
            $if(NIXPACKS_PHP_FALLBACK_PATH) (
                location / {
                    try_files $uri $uri/ ${NIXPACKS_PHP_FALLBACK_PATH}?$query_string;
                }
            ) else (
                location / {
                    try_files $uri $uri/ /index.php?$query_string;
                }
            )
    
            location = /favicon.ico { access_log off; log_not_found off; }
            location = /robots.txt  { access_log off; log_not_found off; }
    
            $if(IS_LARAVEL) (
                error_page 404 /index.php;
            ) else ()
    
            location ~ \.php$ {
                fastcgi_buffer_size 8k;
                fastcgi_pass 127.0.0.1:9000;
                fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
                include $!{nginx}/conf/fastcgi_params;
                include $!{nginx}/conf/fastcgi.conf;
            }
    
            location ~ /\.(?!well-known).* {
                deny all;
            }
        }
    }
    '''
    
  • Zod filtered array schema

    /**
     * Creates a Zod schema for an array that filters out invalid elements during preprocessing.
     *
     * @template T - Type extending ZodSchema
     * @param {T} schema - The Zod schema to validate individual array elements
     * @returns {ZodEffects<ZodArray<T>>} A new Zod schema that:
     * 1. Converts non-array inputs into single-element arrays
     * 2. Filters out elements that don't match the provided schema
     * 3. Validates the resulting array against the schema
     *
     * @example
     * const numberSchema = makeFilteredArraySchema(z.number());
     * numberSchema.parse(['1', 2, '3', 4]); // Returns [2, 4]
     */
    function makeFilteredArraySchema<T extends ZodSchema>(schema: T) {
      return z.preprocess((val) => {
        const array = Array.isArray(val) ? val : [val];
        return array.filter((item: unknown) => schema.safeParse(item).success);
      }, z.array(schema));
    }
    
  • Typesafe Object.entries()

    export function objectEntries<T extends object>(obj: T): [keyof T, T[keyof T]][] {
      return Object.entries(obj) as [keyof T, T[keyof T]][];
    }
    
  • TanStack Query setup for offline caching

    Code snippet to easily setup offline caching for TanStack Query in a React Native app.

    import { persister } from "@/lib/mmkv";
    import { QueryClient, onlineManager } from "@tanstack/react-query";
    import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
    import { useEffect, type PropsWithChildren } from "react";
    import { useReactQueryDevTools } from "@dev-plugins/react-query";
    import NetInfo from "@react-native-community/netinfo";
    
    const queryClient = new QueryClient({
      defaultOptions: {
        queries: {
          staleTime: Number.POSITIVE_INFINITY,
          gcTime: Number.POSITIVE_INFINITY,
        },
      },
    });
    
    export function QueryProvider({ children }: PropsWithChildren) {
      useReactQueryDevTools(queryClient);
    
      useEffect(() => {
        return NetInfo.addEventListener((state) => {
          const status = !!state.isConnected;
          onlineManager.setOnline(status);
        });
      }, []);
    
      return (
        <PersistQueryClientProvider
          client={queryClient}
          onSuccess={() =>
            queryClient.resumePausedMutations().then(() => queryClient.invalidateQueries())
          }
          persistOptions={{ persister, maxAge: Number.POSITIVE_INFINITY }}
        >
          {children}
        </PersistQueryClientProvider>
      );
    }