import { createClient } from '@supabase/supabase-js'

const supabase = createClient(URL, KEY)

export default async function Orders() {
  const { data: orders } = await supabase
    .from('orders')
    .select('id, table, items, status')
    .order('created_at', { ascending: false })

  return (
    <Dashboard title="Live orders">
      {orders.map((o) => (
        <OrderCard key={o.id} order={o} />
      ))}
    </Dashboard>
  )
}

// realtime: new orders slide in
supabase.channel('orders')
  .on('postgres_changes', { event: 'INSERT' },
    (p) => setOrders((prev) => [p.new, ...prev]))
  .subscribe()

const theme = {
  orange: '#FF5A1F',
  blue: '#2F6BFF',
  ink: '#0F0E15',
}

export function useBooking(id: string) {
  const [slot, setSlot] = useState<Slot | null>(null)
  useEffect(() => { load(id).then(setSlot) }, [id])
  return slot
}
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(URL, KEY)

export default async function Orders() {
  const { data: orders } = await supabase
    .from('orders')
    .select('id, table, items, status')
    .order('created_at', { ascending: false })

  return (
    <Dashboard title="Live orders">
      {orders.map((o) => (
        <OrderCard key={o.id} order={o} />
      ))}
    </Dashboard>
  )
}

// realtime: new orders slide in
supabase.channel('orders')
  .on('postgres_changes', { event: 'INSERT' },
    (p) => setOrders((prev) => [p.new, ...prev]))
  .subscribe()

const theme = {
  orange: '#FF5A1F',
  blue: '#2F6BFF',
  ink: '#0F0E15',
}

export function useBooking(id: string) {
  const [slot, setSlot] = useState<Slot | null>(null)
  useEffect(() => { load(id).then(setSlot) }, [id])
  return slot
}
import { View, Text, Pressable } from 'react-native'
import Animated, { FadeInUp } from 'react-native-reanimated'

export function Hero({ user }: Props) {
  return (
    <Animated.View entering={FadeInUp.duration(600)}>
      <Text style={s.title}>Hi, {user.name}</Text>
      <Pressable onPress={startProject}>
        <Text style={s.cta}>Start a project →</Text>
      </Pressable>
    </Animated.View>
  )
}

const s = StyleSheet.create({
  title: { fontSize: 32, fontWeight: '800' },
  cta: { color: '#FF5A1F', letterSpacing: 2 },
})

// api/contact.ts
export async function POST(req: Request) {
  const body = await req.json()
  await sendWhatsApp(body.phone, body.message)
  return Response.json({ ok: true })
}

type Project = {
  name: string
  stack: ('expo' | 'next' | 'supabase')[]
  shipped: boolean
}

const projects: Project[] = await getProjects()
console.log(`deployed ${projects.length} apps`)

$ npx expo start --tunnel
› Metro waiting on exp://192.168.1.4:8081
import { View, Text, Pressable } from 'react-native'
import Animated, { FadeInUp } from 'react-native-reanimated'

export function Hero({ user }: Props) {
  return (
    <Animated.View entering={FadeInUp.duration(600)}>
      <Text style={s.title}>Hi, {user.name}</Text>
      <Pressable onPress={startProject}>
        <Text style={s.cta}>Start a project →</Text>
      </Pressable>
    </Animated.View>
  )
}

const s = StyleSheet.create({
  title: { fontSize: 32, fontWeight: '800' },
  cta: { color: '#FF5A1F', letterSpacing: 2 },
})

// api/contact.ts
export async function POST(req: Request) {
  const body = await req.json()
  await sendWhatsApp(body.phone, body.message)
  return Response.json({ ok: true })
}

type Project = {
  name: string
  stack: ('expo' | 'next' | 'supabase')[]
  shipped: boolean
}

const projects: Project[] = await getProjects()
console.log(`deployed ${projects.length} apps`)

$ npx expo start --tunnel
› Metro waiting on exp://192.168.1.4:8081
$ git push origin main
✓ Build completed in 38s
✓ Deployed to production

async function fetchStats() {
  const res = await fetch('/api/stats', {
    next: { revalidate: 60 },
  })
  if (!res.ok) throw new Error('stats failed')
  return res.json() as Promise<Stats>
}

export const metadata = {
  title: 'John Dev — Apps · Websites · Support',
  description: 'Built in Abu Dhabi',
}

create table bookings (
  id uuid primary key default gen_random_uuid(),
  client text not null,
  starts_at timestamptz,
  status text default 'pending'
);

alter table bookings enable row level security;

function formatAED(n: number) {
  return new Intl.NumberFormat('en-AE', {
    style: 'currency', currency: 'AED',
  }).format(n)
}

<Route path="/projects/:slug" element={<Case />} />

// TODO: ship it ✦
export default App
$ git push origin main
✓ Build completed in 38s
✓ Deployed to production

async function fetchStats() {
  const res = await fetch('/api/stats', {
    next: { revalidate: 60 },
  })
  if (!res.ok) throw new Error('stats failed')
  return res.json() as Promise<Stats>
}

export const metadata = {
  title: 'John Dev — Apps · Websites · Support',
  description: 'Built in Abu Dhabi',
}

create table bookings (
  id uuid primary key default gen_random_uuid(),
  client text not null,
  starts_at timestamptz,
  status text default 'pending'
);

alter table bookings enable row level security;

function formatAED(n: number) {
  return new Intl.NumberFormat('en-AE', {
    style: 'currency', currency: 'AED',
  }).format(n)
}

<Route path="/projects/:slug" element={<Case />} />

// TODO: ship it ✦
export default App
John Josol
01
02
03
Apps · Websites · Support

I design and build mobile apps, websites and dashboards that help growing businesses in the UAE run simpler.

Start a project→
“

Simple software that saves businesses time — designed, built and supported by one person you can actually call.

John Josol
React Native
Next.js
Supabase
Scroll to explore
Available — Abu Dhabi

EN ▾
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(URL, KEY)

export default async function Orders() {
  const { data: orders } = await supabase
    .from('orders')
    .select('id, table, items, status')
    .order('created_at', { ascending: false })

  return (
    <Dashboard title="Live orders">
      {orders.map((o) => (
        <OrderCard key={o.id} order={o} />
      ))}
    </Dashboard>
  )
}

// realtime: new orders slide in
supabase.channel('orders')
  .on('postgres_changes', { event: 'INSERT' },
    (p) => setOrders((prev) => [p.new, ...prev]))
  .subscribe()

const theme = {
  orange: '#FF5A1F',
  blue: '#2F6BFF',
  ink: '#0F0E15',
}

export function useBooking(id: string) {
  const [slot, setSlot] = useState<Slot | null>(null)
  useEffect(() => { load(id).then(setSlot) }, [id])
  return slot
}
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(URL, KEY)

export default async function Orders() {
  const { data: orders } = await supabase
    .from('orders')
    .select('id, table, items, status')
    .order('created_at', { ascending: false })

  return (
    <Dashboard title="Live orders">
      {orders.map((o) => (
        <OrderCard key={o.id} order={o} />
      ))}
    </Dashboard>
  )
}

// realtime: new orders slide in
supabase.channel('orders')
  .on('postgres_changes', { event: 'INSERT' },
    (p) => setOrders((prev) => [p.new, ...prev]))
  .subscribe()

const theme = {
  orange: '#FF5A1F',
  blue: '#2F6BFF',
  ink: '#0F0E15',
}

export function useBooking(id: string) {
  const [slot, setSlot] = useState<Slot | null>(null)
  useEffect(() => { load(id).then(setSlot) }, [id])
  return slot
}
import { View, Text, Pressable } from 'react-native'
import Animated, { FadeInUp } from 'react-native-reanimated'

export function Hero({ user }: Props) {
  return (
    <Animated.View entering={FadeInUp.duration(600)}>
      <Text style={s.title}>Hi, {user.name}</Text>
      <Pressable onPress={startProject}>
        <Text style={s.cta}>Start a project →</Text>
      </Pressable>
    </Animated.View>
  )
}

const s = StyleSheet.create({
  title: { fontSize: 32, fontWeight: '800' },
  cta: { color: '#FF5A1F', letterSpacing: 2 },
})

// api/contact.ts
export async function POST(req: Request) {
  const body = await req.json()
  await sendWhatsApp(body.phone, body.message)
  return Response.json({ ok: true })
}

type Project = {
  name: string
  stack: ('expo' | 'next' | 'supabase')[]
  shipped: boolean
}

const projects: Project[] = await getProjects()
console.log(`deployed ${projects.length} apps`)

$ npx expo start --tunnel
› Metro waiting on exp://192.168.1.4:8081
import { View, Text, Pressable } from 'react-native'
import Animated, { FadeInUp } from 'react-native-reanimated'

export function Hero({ user }: Props) {
  return (
    <Animated.View entering={FadeInUp.duration(600)}>
      <Text style={s.title}>Hi, {user.name}</Text>
      <Pressable onPress={startProject}>
        <Text style={s.cta}>Start a project →</Text>
      </Pressable>
    </Animated.View>
  )
}

const s = StyleSheet.create({
  title: { fontSize: 32, fontWeight: '800' },
  cta: { color: '#FF5A1F', letterSpacing: 2 },
})

// api/contact.ts
export async function POST(req: Request) {
  const body = await req.json()
  await sendWhatsApp(body.phone, body.message)
  return Response.json({ ok: true })
}

type Project = {
  name: string
  stack: ('expo' | 'next' | 'supabase')[]
  shipped: boolean
}

const projects: Project[] = await getProjects()
console.log(`deployed ${projects.length} apps`)

$ npx expo start --tunnel
› Metro waiting on exp://192.168.1.4:8081
$ git push origin main
✓ Build completed in 38s
✓ Deployed to production

async function fetchStats() {
  const res = await fetch('/api/stats', {
    next: { revalidate: 60 },
  })
  if (!res.ok) throw new Error('stats failed')
  return res.json() as Promise<Stats>
}

export const metadata = {
  title: 'John Dev — Apps · Websites · Support',
  description: 'Built in Abu Dhabi',
}

create table bookings (
  id uuid primary key default gen_random_uuid(),
  client text not null,
  starts_at timestamptz,
  status text default 'pending'
);

alter table bookings enable row level security;

function formatAED(n: number) {
  return new Intl.NumberFormat('en-AE', {
    style: 'currency', currency: 'AED',
  }).format(n)
}

<Route path="/projects/:slug" element={<Case />} />

// TODO: ship it ✦
export default App
$ git push origin main
✓ Build completed in 38s
✓ Deployed to production

async function fetchStats() {
  const res = await fetch('/api/stats', {
    next: { revalidate: 60 },
  })
  if (!res.ok) throw new Error('stats failed')
  return res.json() as Promise<Stats>
}

export const metadata = {
  title: 'John Dev — Apps · Websites · Support',
  description: 'Built in Abu Dhabi',
}

create table bookings (
  id uuid primary key default gen_random_uuid(),
  client text not null,
  starts_at timestamptz,
  status text default 'pending'
);

alter table bookings enable row level security;

function formatAED(n: number) {
  return new Intl.NumberFormat('en-AE', {
    style: 'currency', currency: 'AED',
  }).format(n)
}

<Route path="/projects/:slug" element={<Case />} />

// TODO: ship it ✦
export default App
John Josol
React Native
Supabase
Apps · Websites · Support

I design and build mobile apps, websites and dashboards that help growing businesses in the UAE run simpler.

Start a project→
Available — Abu Dhabi
SCROLL ↓

About me

hello, world. I'm John. Developer · Abu Dhabi, UAE. I design, build and support apps, websites and dashboards for small businesses — and keep them running. Years in operations before code, now shipping with React Native, Expo, Next.js and Supabase.

What I do

~/PROCESS

HOW I WORK

One person from first call to launch — and after.

  1. DAY 1–3

    Discover

    A quick call to understand your business, your customers and what a win looks like. You get a clear scope and price.

    CALLGOALSSCOPE
  2. WEEK 1–2

    Design

    Wireframes, then a clickable Figma prototype you can tap through on your phone before a single line of code.

    WIREFRAMESUIPROTOTYPE
  3. WEEK 2–6

    Build

    I build it with React Native, Next.js and Supabase, and show you real progress every week.

    WEEKLY DEMOSTESTINGLAUNCH
  4. ONGOING

    Support

    After launch I host it, watch it and keep improving it. One contact, no hand-offs.

    HOSTINGFIXESUPDATES
~/PROCESS

HOW I WORK

One person from first call to launch — and after.

  1. DAY 1–3

    Discover

    A quick call to understand your business, your customers and what a win looks like.

    CALLGOALSSCOPE
  2. WEEK 1–2

    Design

    Wireframes, then a clickable Figma prototype you can tap through before any code.

    WIREFRAMESUIPROTOTYPE
  3. WEEK 2–6

    Build

    React Native, Next.js and Supabase, with real progress shown every week.

    WEEKLY DEMOSTESTINGLAUNCH
  4. ONGOING

    Support

    After launch I host it, watch it and keep improving it. One contact, no hand-offs.

    HOSTINGFIXESUPDATES
~/PROOF

BY THE NUMBERS

Small team of one. Real things shipped.

PROJECTS SHIPPEDApps, sites and systems
SERVICESDesign → build → support
PLATFORMSiOS · Android · Web
POINT OF CONTACTYou talk to the person building it
BUILT WITH
React NativeExpoNext.jsTypeScriptSupabasePostgreSQLNode.jsTailwindFigmaVercelUbuntu serversOpenAI
OpenAIUbuntu serversVercelFigmaTailwindNode.jsPostgreSQLSupabaseTypeScriptNext.jsExpoReact Native
~/PROOF

BY THE NUMBERS

Small team of one. Real things shipped.

PROJECTS SHIPPEDApps, sites and systems
SERVICESDesign → build → support
PLATFORMSiOS · Android · Web
POINT OF CONTACTYou talk to the builder
BUILT WITH
React NativeExpoNext.jsTypeScriptSupabasePostgreSQLNode.jsTailwindFigmaVercelUbuntu serversOpenAI
OpenAIUbuntu serversVercelFigmaTailwindNode.jsPostgreSQLSupabaseTypeScriptNext.jsExpoReact Native
~/CONTACT  ·  LET'S TALK

LET'S BUILDsomething good.

Tell me what you want to build. You'll talk to me directly — from the first message to launch day and after.

PLACEHOLDER — add WHATSAPP_NUMBER in app/_lib/site.tsMessage me on WhatsApp →See projects ↑AVAILABLE FOR NEW PROJECTS — ABU DHABI
~/CONTACT  ·  LET'S TALK

LET'SBUILDsomethinggood.

Tell me what you want to build. You'll talk to me directly — from the first message to launch day and after.

PLACEHOLDER — add WHATSAPP_NUMBER in app/_lib/site.tsMessage me on WhatsApp →See projects ↑AVAILABLE — ABU DHABI