diff --git a/components/DetailChart.tsx b/components/DetailChart.tsx
index bd50894..9d174a8 100644
--- a/components/DetailChart.tsx
+++ b/components/DetailChart.tsx
@@ -34,7 +34,7 @@ export default function DetailChart({
state: MonitorState
}) {
const { t } = useTranslation('common')
- const latencyData = state.latency[monitor.id].recent.map((point) => ({
+ const latencyData = state.latency[monitor.id].map((point) => ({
x: point.time * 1000,
y: point.ping,
loc: point.loc,
diff --git a/components/MaintenanceAlert.tsx b/components/MaintenanceAlert.tsx
index b460046..746d80e 100644
--- a/components/MaintenanceAlert.tsx
+++ b/components/MaintenanceAlert.tsx
@@ -70,7 +70,9 @@ export default function MaintenanceAlert({
{upcoming ? t('Expected end') : t('To')}
- {maintenance.end ? new Date(maintenance.end).toLocaleString() : t('Until further notice')}
+ {maintenance.end
+ ? new Date(maintenance.end).toLocaleString()
+ : t('Until further notice')}
diff --git a/init.sql b/init.sql
index 2cb27f4..bdfe36a 100644
--- a/init.sql
+++ b/init.sql
@@ -2,7 +2,3 @@ CREATE TABLE IF NOT EXISTS uptimeflare (
key VARCHAR(255) PRIMARY KEY,
value BLOB NOT NULL
);
-
-REPLACE INTO uptimeflare (key, value) VALUES ('state', '{"test": 1}');
-
-INSERT INTO uptimeflare (key, value) VALUES ('state', '{"test": 1}') ON CONFLICT(key) DO UPDATE SET value = excluded.value;
diff --git a/pages/api/badge.ts b/pages/api/badge.ts
index 65bacce..2ba6f69 100644
--- a/pages/api/badge.ts
+++ b/pages/api/badge.ts
@@ -61,7 +61,7 @@ export default async function handler(req: NextRequest): Promise {
const state = JSON.parse(stateStr) as MonitorState
const monitorIncidentHistory = state.incident?.[monitorId]
- const hasLatencyData = Boolean(state.latency?.[monitorId]?.recent?.length)
+ const hasLatencyData = Boolean(state.latency?.[monitorId]?.length)
if (!monitorIncidentHistory || monitorIncidentHistory.length === 0 || !hasLatencyData) {
return new Response(JSON.stringify(errorBadge(label, 'unknown')), {
diff --git a/pages/api/data.ts b/pages/api/data.ts
index fd7fe1e..88e74bb 100644
--- a/pages/api/data.ts
+++ b/pages/api/data.ts
@@ -29,8 +29,8 @@ export default async function handler(req: NextRequest): Promise {
const isUp = state.incident[monitor.id].slice(-1)[0].end !== undefined
monitors[monitor.id] = {
up: isUp,
- latency: state.latency[monitor.id].recent.slice(-1)[0].ping,
- location: state.latency[monitor.id].recent.slice(-1)[0].loc,
+ latency: state.latency[monitor.id].slice(-1)[0].ping,
+ location: state.latency[monitor.id].slice(-1)[0].loc,
message: isUp ? 'OK' : state.incident[monitor.id].slice(-1)[0].error.slice(-1)[0],
}
}
diff --git a/pages/index.tsx b/pages/index.tsx
index 94ca3cf..976a958 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -57,9 +57,7 @@ export default function Home({
{state == undefined ? (
-
- {t('Monitor State not defined')}
-
+ {t('Monitor State not defined')}
) : (
@@ -76,7 +74,7 @@ export default function Home({
export async function getServerSideProps() {
// Read state as string from KV, to avoid hitting server-side cpu time limit
- const state = await getFromStore(process.env as any, 'state') as unknown as MonitorState
+ const state = (await getFromStore(process.env as any, 'state')) as unknown as MonitorState
// Only present these values to client
const monitors = workerConfig.monitors.map((monitor) => {
diff --git a/types/config.ts b/types/config.ts
index 7933c01..eb5ec7d 100644
--- a/types/config.ts
+++ b/types/config.ts
@@ -92,32 +92,61 @@ export type Callbacks
= {
) => Promise | any
}
+export type IncidentRecord = {
+ start: number[]
+ end: number | null // null if it's still open
+ error: string[]
+}
+
+export type LatencyRecord = {
+ loc: string
+ ping: number
+ time: number
+}
+
export type MonitorState = {
lastUpdate: number
overallUp: number
overallDown: number
+ incident: Record
+ latency: Record // recent 12 hour data, N min interval
+}
+
+// This is now the actual stored format (after TODO D1 migration) to improve (de)serialization performance
+// This gives a ~3.5x speedup in computing and a 58% reduction in size
+// The CPULimitExceeded issue with 10+ monitors on free tier should be largely mitigated by this change
+// local profiling result (1 op = parse + stringify):
+// MonitorState (original): 277 ops/s, ±0.51% | slowest, 71.09% slower
+// MonitorStateCompacted: 958 ops/s, ±1.17% | fastest
+export type MonitorStateCompacted = {
+ lastUpdate: number
+ overallUp: number
+ overallDown: number
+
+ // incident in stored in columnar format
incident: Record<
- string,
+ string, // monitor id
{
- start: number[]
- end: number | undefined // undefined if it's still open
- error: string[]
- }[]
+ start: number[][]
+ end: (number | null)[]
+ error: string[][]
+ }
>
+ // latency in stored in columnar format
+ // also uses Run-length encoding for loc & Base64 encoding for number arrays
latency: Record<
- string,
+ string, // monitor id
{
- recent: {
- loc: string
- ping: number
- time: number
- }[] // recent 12 hour data, 2 min interval
- all: {
- loc: string
- ping: number
- time: number
- }[] // all data in 90 days, 1 hour interval
+ loc: {
+ v: string[] // RLE values
+ c: number[] // RLE counts
+ }
+ // Hex results in a larger size and slower encoding/decoding than base64,
+ // but we can pop/append arbitrary number of bytes without decoding then re-encoding the whole string
+ // This is useful in Workers and shows a ~2% speedup comapred to base64, and it also simplifies the code
+ ping: string // Hex encoded Uint16Array
+ time: string // Hex encoded Uint32Array
}
>
}
diff --git a/util/i18n.ts b/util/i18n.ts
index d3ed910..f3ffbb3 100644
--- a/util/i18n.ts
+++ b/util/i18n.ts
@@ -1,8 +1,8 @@
-import i18n from 'i18next';
-import { initReactI18next } from 'react-i18next';
-import LanguageDetector from 'i18next-browser-languagedetector';
-import en from '../locales/en/common.json';
-import zhCN from '../locales/zh-CN/common.json';
+import i18n from 'i18next'
+import { initReactI18next } from 'react-i18next'
+import LanguageDetector from 'i18next-browser-languagedetector'
+import en from '../locales/en/common.json'
+import zhCN from '../locales/zh-CN/common.json'
i18n
.use(LanguageDetector)
@@ -20,6 +20,6 @@ i18n
detection: {
order: ['navigator'],
},
- });
+ })
-export default i18n;
+export default i18n
diff --git a/worker/src/index.ts b/worker/src/index.ts
index ae02ef1..955203b 100644
--- a/worker/src/index.ts
+++ b/worker/src/index.ts
@@ -1,9 +1,9 @@
import { DurableObject } from 'cloudflare:workers'
-import { MonitorState, MonitorTarget } from '../../types/config'
-import { maintenances, workerConfig } from '../../uptime.config'
+import { MonitorTarget } from '../../types/config'
+import { workerConfig } from '../../uptime.config'
import { getStatus, getStatusWithGlobalPing } from './monitor'
-import { formatStatusChangeNotification, getWorkerLocation, webhookNotify } from './util'
-import { getFromStore, setToStore } from './store'
+import { formatAndNotify, getWorkerLocation } from './util'
+import { CompactedMonitorStateWrapper, getFromStore, setToStore } from './store'
export interface Env {
UPTIMEFLARE_STATE: KVNamespace
@@ -16,66 +16,12 @@ const Worker = {
const workerLocation = (await getWorkerLocation()) || 'ERROR'
console.log(`Running scheduled event on ${workerLocation}...`)
- // Auxiliary function to format notification and send it via webhook
- let formatAndNotify = async (
- monitor: MonitorTarget,
- isUp: boolean,
- timeIncidentStart: number,
- timeNow: number,
- reason: string
- ) => {
- // Skip notification if monitor is in the skip list
- const skipList = workerConfig.notification?.skipNotificationIds
- if (skipList && skipList.includes(monitor.id)) {
- console.log(
- `Skipping notification for ${monitor.name} (${monitor.id} in skipNotificationIds)`
- )
- return
- }
-
- // Skip notification if monitor is in maintenance
- const maintenanceList = maintenances
- .filter(
- (m) =>
- new Date(timeNow * 1000) >= new Date(m.start) &&
- (!m.end || new Date(timeNow * 1000) <= new Date(m.end))
- )
- .map((e) => e.monitors || [])
- .flat()
-
- if (maintenanceList.includes(monitor.id)) {
- console.log(`Skipping notification for ${monitor.name} (in maintenance)`)
- return
- }
-
- if (workerConfig.notification?.webhook) {
- const notification = formatStatusChangeNotification(
- monitor,
- isUp,
- timeIncidentStart,
- timeNow,
- reason,
- workerConfig.notification?.timeZone ?? 'Etc/GMT'
- )
- await webhookNotify(workerConfig.notification.webhook, notification)
- } else {
- console.log(`Webhook not set, skipping notification for ${monitor.name}`)
- }
- }
-
- // Read state, set init state if it doesn't exist
- let state = JSON.parse(await getFromStore(env, 'state') || '{}') as unknown as MonitorState
- if (!state || Object.keys(state).length === 0) {
- state = {
- lastUpdate: 0,
- overallUp: 0,
- overallDown: 0,
- incident: {},
- latency: {},
- }
- }
- state.overallDown = 0
- state.overallUp = 0
+ // Create a wrapped MonitorState from stored compacted state
+ const state = new CompactedMonitorStateWrapper(
+ await getFromStore(env, 'state')
+ )
+ state.data.overallDown = 0
+ state.data.overallUp = 0
let statusChanged = false
const currentTimeSecond = Math.round(Date.now() / 1000)
@@ -126,6 +72,7 @@ const Worker = {
console.log('Falling back to local check...')
status = await getStatus(monitor)
} else {
+ // TODO: more consistent error handling (throw or return?)
status = { ping: 0, up: false, err: 'Unknown check proxy error' }
}
}
@@ -134,29 +81,32 @@ const Worker = {
status = await getStatus(monitor)
}
- // const status = await getStatus(monitor)
const currentTimeSecond = Math.round(Date.now() / 1000)
// Update counters
- status.up ? state.overallUp++ : state.overallDown++
+ status.up ? state.data.overallUp++ : state.data.overallDown++
// Update incidents
// Create a dummy incident to store the start time of the monitoring and simplify logic
- state.incident[monitor.id] = state.incident[monitor.id] || [
- {
+ if (state.incidentLen(monitor.id) === 0) {
+ state.appendIncident(monitor.id, {
start: [currentTimeSecond],
end: currentTimeSecond,
error: ['dummy'],
- },
- ]
- // Then lastIncident here must not be undefined
- let lastIncident = state.incident[monitor.id].slice(-1)[0]
+ })
+ }
+
+ // Then lastIncident here must not be null
+ let lastIncident = state.getIncident(monitor.id, state.incidentLen(monitor.id) - 1)
if (status.up) {
// Current status is up
// close existing incident if any
- if (lastIncident.end === undefined) {
+ if (lastIncident.end === null) {
lastIncident.end = currentTimeSecond
+ // write back the modified last incident
+ state.setIncident(monitor.id, state.incidentLen(monitor.id) - 1, lastIncident)
+
monitorStatusChanged = true
try {
if (
@@ -190,24 +140,30 @@ const Worker = {
} else {
// Current status is down
// open new incident if not already open
- if (lastIncident.end !== undefined) {
- state.incident[monitor.id].push({
+ if (lastIncident.end !== null) {
+ state.appendIncident(monitor.id, {
start: [currentTimeSecond],
- end: undefined,
+ end: null,
error: [status.err],
})
monitorStatusChanged = true
} else if (
- lastIncident.end === undefined &&
+ lastIncident.end === null &&
lastIncident.error.slice(-1)[0] !== status.err
) {
// append if the error message changes
lastIncident.start.push(currentTimeSecond)
lastIncident.error.push(status.err)
+
+ // write back the modified last incident
+ state.setIncident(monitor.id, state.incidentLen(monitor.id) - 1, lastIncident)
monitorStatusChanged = true
}
- const currentIncident = state.incident[monitor.id].slice(-1)[0]
+ const currentIncident = state.getIncident(
+ monitor.id,
+ state.incidentLen(monitor.id) - 1
+ )
try {
if (
// monitor status changed AND...
@@ -284,63 +240,54 @@ const Worker = {
}
// append to latency data
- let latencyLists = state.latency[monitor.id] || {
- recent: [],
- }
- latencyLists.all = []
-
- const record = {
+ state.appendLatency(monitor.id,{
loc: checkLocation,
ping: status.ping,
time: currentTimeSecond,
- }
- latencyLists.recent.push(record)
+ })
// discard old data
- while (latencyLists.recent[0]?.time < currentTimeSecond - 12 * 60 * 60) {
- latencyLists.recent.shift()
+ while (state.getFirstLatency(monitor.id).time < currentTimeSecond - 12 * 60 * 60) {
+ state.unshiftLatency(monitor.id)
}
- state.latency[monitor.id] = latencyLists
// discard old incidents
- let incidentList = state.incident[monitor.id]
while (
- incidentList.length > 0 &&
- incidentList[0].end &&
- incidentList[0].end < currentTimeSecond - 90 * 24 * 60 * 60
+ state.incidentLen(monitor.id) > 0 &&
+ state.getIncident(monitor.id, 0).end &&
+ state.getIncident(monitor.id, 0).end! < currentTimeSecond - 90 * 24 * 60 * 60
) {
- incidentList.shift()
+ state.shiftIncident(monitor.id)
}
if (
- incidentList.length == 0 ||
- (incidentList[0].start[0] > currentTimeSecond - 90 * 24 * 60 * 60 &&
- incidentList[0].error[0] != 'dummy')
+ state.incidentLen(monitor.id) === 0 ||
+ (state.getIncident(monitor.id, 0).start[0] > currentTimeSecond - 90 * 24 * 60 * 60 &&
+ state.getIncident(monitor.id, 0).error[0] != 'dummy')
) {
// put the dummy incident back
- incidentList.unshift({
+ state.unshiftIncident(monitor.id, {
start: [currentTimeSecond - 90 * 24 * 60 * 60],
end: currentTimeSecond - 90 * 24 * 60 * 60,
error: ['dummy'],
})
}
- state.incident[monitor.id] = incidentList
statusChanged ||= monitorStatusChanged
}
console.log(
- `statusChanged: ${statusChanged}, lastUpdate: ${state.lastUpdate}, currentTime: ${currentTimeSecond}`
+ `statusChanged: ${statusChanged}, lastUpdate: ${state.data.lastUpdate}, currentTime: ${currentTimeSecond}`
)
// Update state
- // Allow for a cooldown period before writing to KV
+ // Allow for a cooldown period before writing to storage
if (
statusChanged ||
- currentTimeSecond - state.lastUpdate >= (workerConfig.kvWriteCooldownMinutes ?? 3) * 60 - 10 // Allow for 10 seconds of clock drift
+ currentTimeSecond - state.data.lastUpdate >= (workerConfig.kvWriteCooldownMinutes ?? 3) * 60 - 10 // Allow for 10 seconds of clock drift
) {
console.log('Updating state...')
- state.lastUpdate = currentTimeSecond
- await setToStore(env, 'state', JSON.stringify(state))
+ state.data.lastUpdate = currentTimeSecond
+ await setToStore(env, 'state', state.getCompactedStateStr())
} else {
console.log('Skipping state update due to cooldown period.')
}
diff --git a/worker/src/monitor.ts b/worker/src/monitor.ts
index eb9be2e..4ad4e77 100644
--- a/worker/src/monitor.ts
+++ b/worker/src/monitor.ts
@@ -304,7 +304,9 @@ export async function getStatus(
response.status,
response.text.bind(response)
)
- try { await response.body?.cancel() } catch(e) {} // Always try to cancel body, see issue #166
+ try {
+ await response.body?.cancel()
+ } catch (e) {} // Always try to cancel body, see issue #166
if (err !== null) {
console.log(`${monitor.name} didn't pass response check: ${err}`)
diff --git a/worker/src/store.ts b/worker/src/store.ts
index fed56f2..775401b 100644
--- a/worker/src/store.ts
+++ b/worker/src/store.ts
@@ -1,17 +1,155 @@
-import { Env } from ".";
+import { Env } from '.'
+import { IncidentRecord, LatencyRecord, MonitorStateCompacted } from '../../types/config'
export async function getFromStore(env: Env, key: string): Promise {
- const stmt = env.UPTIMEFLARE_D1.prepare("SELECT value FROM uptimeflare WHERE key = ?")
- const result = await stmt.bind(key).first<{ value: string }>()
-
- if (!result) {
- return await env.UPTIMEFLARE_STATE.get(key)
- }
-
- return result?.value || null
+ const stmt = env.UPTIMEFLARE_D1.prepare('SELECT value FROM uptimeflare WHERE key = ?')
+ const result = await stmt.bind(key).first<{ value: string }>()
+ return result?.value || null
}
export async function setToStore(env: Env, key: string, value: string): Promise {
- const stmt = env.UPTIMEFLARE_D1.prepare("INSERT INTO uptimeflare (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value;")
- await stmt.bind(key, value).run()
+ const stmt = env.UPTIMEFLARE_D1.prepare(
+ 'INSERT INTO uptimeflare (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value;'
+ )
+ await stmt.bind(key, value).run()
+}
+
+export class CompactedMonitorStateWrapper {
+ data: MonitorStateCompacted
+
+ constructor(compactedStateStr: string | null) {
+ if (!compactedStateStr) {
+ // Initialize empty state
+ this.data = {
+ lastUpdate: 0,
+ overallUp: 0,
+ overallDown: 0,
+ incident: {},
+ latency: {},
+ }
+ return
+ }
+ this.data = JSON.parse(compactedStateStr)
+ }
+
+ getCompactedStateStr(): string {
+ return JSON.stringify(this.data)
+ }
+
+ incidentLen(monitorId: string): number {
+ const incidents = this.data.incident[monitorId]
+ if (!incidents) return 0
+ return incidents.start.length
+ }
+
+ getIncident(monitorId: string, index: number): IncidentRecord {
+ const incidents = this.data.incident[monitorId]
+ if (!incidents || index < 0 || index >= incidents.start.length) {
+ throw new Error('Index out of bounds or monitor not found')
+ }
+ return {
+ start: incidents.start[index],
+ end: incidents.end[index],
+ error: incidents.error[index],
+ }
+ }
+
+ setIncident(monitorId: string, index: number, incident: IncidentRecord) {
+ const incidents = this.data.incident[monitorId]
+ if (!incidents || index < 0 || index >= incidents.start.length) {
+ throw new Error('Index out of bounds or monitor not found')
+ }
+ incidents.start[index] = incident.start
+ incidents.end[index] = incident.end
+ incidents.error[index] = incident.error
+ }
+
+ appendIncident(monitorId: string, incident: IncidentRecord) {
+ let incidents = this.data.incident[monitorId]
+ if (!incidents) {
+ // Initialize incident arrays
+ this.data.incident[monitorId] = {
+ start: [],
+ end: [],
+ error: [],
+ }
+ incidents = this.data.incident[monitorId]
+ }
+ incidents.start.push(incident.start)
+ incidents.end.push(incident.end)
+ incidents.error.push(incident.error)
+ }
+
+ shiftIncident(monitorId: string) {
+ const incidents = this.data.incident[monitorId]
+ incidents.start.shift()
+ incidents.end.shift()
+ incidents.error.shift()
+ }
+
+ unshiftIncident(monitorId: string, incident: IncidentRecord) {
+ const incidents = this.data.incident[monitorId]
+ incidents.start.unshift(incident.start)
+ incidents.end.unshift(incident.end)
+ incidents.error.unshift(incident.error)
+ }
+
+ latencyLen(monitorId: string): number {
+ const latencies = this.data.latency[monitorId]
+ if (!latencies) return 0
+ return latencies.ping.length / 4 // Uint16Array, 4 characters per entry in hex
+ }
+
+ appendLatency(monitorId: string, record: LatencyRecord) {
+ let latencies = this.data.latency[monitorId]
+ if (!latencies) {
+ // Initialize latency arrays
+ this.data.latency[monitorId] = {
+ time: '',
+ ping: '',
+ loc: {
+ c: [],
+ v: [],
+ },
+ }
+ latencies = this.data.latency[monitorId]
+ }
+
+ // @ts-expect-error
+ latencies.time += new Uint8Array(new Uint32Array([record.time]).buffer).toHex()
+ // @ts-expect-error
+ latencies.ping += new Uint8Array(new Uint16Array([record.ping]).buffer).toHex()
+
+ if (latencies.loc.v[latencies.loc.v.length - 1] !== record.loc) {
+ latencies.loc.c.push(1)
+ latencies.loc.v.push(record.loc)
+ } else {
+ latencies.loc.c[latencies.loc.c.length - 1] += 1
+ }
+ }
+
+ getFirstLatency(monitorId: string): LatencyRecord {
+ let latencies = this.data.latency[monitorId]
+
+ return {
+ // @ts-expect-error
+ time: new Uint32Array(Uint8Array.fromHex(latencies.time.slice(0, 8)).buffer)[0],
+ // @ts-expect-error
+ ping: new Uint16Array(Uint8Array.fromHex(latencies.ping.slice(0, 4)).buffer)[0],
+ loc: latencies.loc.v[0],
+ }
+ }
+
+ unshiftLatency(monitorId: string) {
+ let latencies = this.data.latency[monitorId]
+
+ latencies.time = latencies.time.slice(8)
+ latencies.ping = latencies.ping.slice(4)
+
+ latencies.loc.c[0] -= 1
+ if (latencies.loc.c[0] === 0) {
+ latencies.loc.c.shift()
+ latencies.loc.v.shift()
+ }
+ }
}
diff --git a/worker/src/util.ts b/worker/src/util.ts
index 0cbe5ad..bd18c50 100644
--- a/worker/src/util.ts
+++ b/worker/src/util.ts
@@ -1,4 +1,5 @@
-import { WebhookConfig } from '../../types/config'
+import { MonitorTarget, WebhookConfig } from '../../types/config'
+import { maintenances, workerConfig } from '../../uptime.config'
async function getWorkerLocation() {
const res = await fetch('https://cloudflare.com/cdn-cgi/trace')
@@ -145,10 +146,59 @@ async function webhookNotify(webhook: WebhookConfig, message: string) {
}
}
+// Auxiliary function to format notification and send it via webhook
+const formatAndNotify = async (
+ monitor: MonitorTarget,
+ isUp: boolean,
+ timeIncidentStart: number,
+ timeNow: number,
+ reason: string
+) => {
+ // Skip notification if monitor is in the skip list
+ const skipList = workerConfig.notification?.skipNotificationIds
+ if (skipList && skipList.includes(monitor.id)) {
+ console.log(
+ `Skipping notification for ${monitor.name} (${monitor.id} in skipNotificationIds)`
+ )
+ return
+ }
+
+ // Skip notification if monitor is in maintenance
+ const maintenanceList = maintenances
+ .filter(
+ (m) =>
+ new Date(timeNow * 1000) >= new Date(m.start) &&
+ (!m.end || new Date(timeNow * 1000) <= new Date(m.end))
+ )
+ .map((e) => e.monitors || [])
+ .flat()
+
+ if (maintenanceList.includes(monitor.id)) {
+ console.log(`Skipping notification for ${monitor.name} (in maintenance)`)
+ return
+ }
+
+ if (workerConfig.notification?.webhook) {
+ const notification = formatStatusChangeNotification(
+ monitor,
+ isUp,
+ timeIncidentStart,
+ timeNow,
+ reason,
+ workerConfig.notification?.timeZone ?? 'Etc/GMT'
+ )
+ await webhookNotify(workerConfig.notification.webhook, notification)
+ } else {
+ console.log(`Webhook not set, skipping notification for ${monitor.name}`)
+ }
+}
+
+
export {
getWorkerLocation,
fetchTimeout,
withTimeout,
webhookNotify,
formatStatusChangeNotification,
+ formatAndNotify,
}
diff --git a/worker/wrangler-dev.toml b/worker/wrangler-dev.toml
index 6dcc951..022ce15 100644
--- a/worker/wrangler-dev.toml
+++ b/worker/wrangler-dev.toml
@@ -1,5 +1,9 @@
name = "uptimeflare_worker"
main = "src/index.ts"
compatibility_date = "2025-04-02"
-kv_namespaces = [{ binding = "UPTIMEFLARE_STATE", id = "UPTIMEFLARE_STATE" }]
compatibility_flags = [ "nodejs_compat" ]
+
+[[d1_databases]]
+binding = "UPTIMEFLARE_D1"
+database_name = "uptimeflare_d1"
+database_id = "00000000-0000-0000-0000-000000000000"
diff --git a/worker/wrangler.toml b/worker/wrangler.toml
index eda714e..0ab2b92 100644
--- a/worker/wrangler.toml
+++ b/worker/wrangler.toml
@@ -2,3 +2,4 @@ name = "uptimeflare_worker"
main = "src/index.ts"
compatibility_date = "2025-04-02"
compatibility_flags = [ "nodejs_compat" ]
+
diff --git a/wrangler.toml b/wrangler.toml
new file mode 100644
index 0000000..33c09e7
--- /dev/null
+++ b/wrangler.toml
@@ -0,0 +1,8 @@
+name = "uptimeflare"
+compatibility_date = "2025-04-02"
+compatibility_flags = [ "nodejs_compat" ]
+
+[[d1_databases]]
+binding = "UPTIMEFLARE_D1"
+database_name = "uptimeflare_d1"
+database_id = "00000000-0000-0000-0000-000000000000"