diff --git a/packages/events/LICENSE b/packages/events/LICENSE
new file mode 100644
index 000000000..38b41d975
--- /dev/null
+++ b/packages/events/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Solid Primitives Working Group
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/packages/events/README.md b/packages/events/README.md
new file mode 100644
index 000000000..ffc806646
--- /dev/null
+++ b/packages/events/README.md
@@ -0,0 +1,281 @@
+
+
+
+
+# @solid-primitives/events
+
+[](https://turborepo.org/)
+[](https://bundlephobia.com/package/@solid-primitives/events)
+[](https://www.npmjs.com/package/@solid-primitives/events)
+[](https://github.com/solidjs-community/solid-primitives#contribution-process)
+
+A set of primitives for declarative event composition and state derivation for solidjs. You can think of it as a much simpler version of Rxjs that integrates well with Solidjs.
+
+[Here is an implementation of the Strello demo that uses `solid-events`](https://github.com/devagrawal09/strello/pull/1/files).
+
+## Contents
+- [@solid-primitives/events](#solid-primitivesevents)
+ - [Contents](#contents)
+ - [Installatiom](#installatiom)
+ - [`createEvent`](#createevent)
+ - [Tranformation](#tranformation)
+ - [Disposal](#disposal)
+ - [Halting](#halting)
+ - [Async Events](#async-events)
+ - [`createSubject`](#createsubject)
+ - [`createAsyncSubject`](#createasyncsubject)
+ - [`createSubjectStore`](#createsubjectstore)
+ - [`createTopic`](#createtopic)
+ - [`createPartition`](#createpartition)
+ - [Use Cases](#use-cases)
+
+## Installatiom
+
+```bash
+npm install solid-events
+```
+or
+```bash
+pnpm install solid-events
+```
+or
+```bash
+bun install solid-events
+```
+
+
+## `createEvent`
+
+Returns an event handler and an event emitter. The handler can execute a callback when the event is emitted.
+
+```ts
+const [onEvent, emitEvent] = createEvent()
+
+onEvent(payload => console.log(`Event emitted:`, payload))
+
+...
+
+emitEvent(`Hello World!`)
+// logs "Event emitted: Hello World!"
+```
+
+### Tranformation
+
+The handler can return a new handler with the value returned from the callback. This allows chaining transformations.
+
+```ts
+const [onIncrement, emitIncrement] = createEvent()
+
+const onMessage = onIncrement((delta) => `Increment by ${delta}`)
+
+onMessage(message => console.log(`Message emitted:`, message))
+
+...
+
+emitIncrement(2)
+// logs "Message emitted: Increment by 2"
+```
+
+### Disposal
+Handlers that are called inside a component are automatically cleaned up with the component, so no manual bookeeping is necesarry.
+
+```tsx
+function Counter() {
+ const [onIncrement, emitIncrement] = createEvent()
+
+ const onMessage = onIncrement((delta) => `Increment by ${delta}`)
+
+ onMessage(message => console.log(`Message emitted:`, message))
+
+ return
....
+}
+```
+Calling `onIncrement` and `onMessage` registers a stateful subscription. The lifecycle of these subscriptions are tied to their owner components. This ensures there's no memory leaks.
+
+### Halting
+
+Event propogation can be stopped at any point using `halt()`
+
+```ts
+const [onIncrement, emitIncrement] = createEvent()
+
+const onValidIncrement = onIncrement(delta => delta < 1 ? halt() : delta)
+const onMessage = onValidIncrement((delta) => `Increment by ${delta}`)
+
+onMessage(message => console.log(`Message emitted:`, message))
+
+...
+
+emitIncrement(2)
+// logs "Message emitted: Increment by 2"
+
+...
+
+emitIncrement(0)
+// Doesn't log anything
+```
+
+`halt()` returns a `never`, so typescript correctly infers the return type of the handler.
+
+### Async Events
+
+If you return a promise from an event callback, the resulting event will wait to emit until the promise resolves. In other words, promises are automatically flattened by events.
+
+```ts
+async function createBoard(boardData) {
+ "use server"
+ const boardId = await db.boards.create(boardData)
+ return boardId
+}
+
+const [onCreateBoard, emitCreateBoard] = createEvent()
+
+const onBoardCreated = onCreateBoard(boardData => createBoard(boardData))
+
+onBoardCreated(boardId => navigate(`/board/${boardId}`))
+```
+
+## `createSubject`
+
+Events can be used to derive state using Subjects. A Subject is a signal that can be derived from event handlers.
+
+```ts
+const [onIncrement, emitIncrement] = createEvent()
+const [onReset, emitReset] = createEvent()
+
+const onMessage = onIncrement((delta) => `Increment by ${delta}`)
+onMessage(message => console.log(`Message emitted:`, message))
+
+const count = createSubject(
+ 0,
+ onIncrement(delta => currentCount => currentCount + delta),
+ onReset(() => 0)
+)
+
+createEffect(() => console.log(`count`, count()))
+
+...
+
+emitIncrement(2)
+// logs "Message emitted: Increment by 2"
+// logs "count 2"
+
+emitReset()
+// logs "count 0"
+```
+
+To update the value of a subject, event handlers can return a value (like `onReset`), or a function that transforms the current value (like `onIncrement`).
+
+`createSubject` can also accept a signal as the first input instead of a static value. The subject's value resets whenever the source signal updates.
+
+```tsx
+function Counter(props) {
+ const [onIncrement, emitIncrement] = createEvent()
+ const [onReset, emitReset] = createEvent()
+
+ const count = createSubject(
+ () => props.count,
+ onIncrement(delta => currentCount => currentCount + delta),
+ onReset(() => 0)
+ )
+
+ return
...
+}
+```
+
+`createSubject` has some compound variations to complete use cases.
+
+### `createAsyncSubject`
+
+This subject accepts a reactive async function as the first argument similar to `createAsync`, and resets whenever the function reruns.
+
+```ts
+const getBoards = cache(async () => {
+ "use server";
+ // fetch from database
+}, "get-boards");
+
+export default function HomePage() {
+ const [onDeleteBoard, emitDeleteBoard] = createEvent();
+
+ const boards = createAsyncSubject(
+ () => getBoards(),
+ onDeleteBoard(
+ (boardId) => (boards) => boards.filter((board) => board.id !== boardId)
+ )
+ );
+
+ ...
+}
+```
+
+### `createSubjectStore`
+
+This subject is a store instead of a regular signal. Event handlers can mutate the current state of the board directly. Uses `produce` under the hood.
+
+```ts
+const boardStore = createSubjectStore(
+ () => boardData(),
+ onCreateNote((createdNote) => (board) => {
+ const index = board.notes.findIndex((n) => n.id === note.id);
+ if (index === -1) board.notes.push(note);
+ }),
+ onDeleteNote(([id]) => (board) => {
+ const index = board.notes.findIndex((n) => n.id === id);
+ if (index !== -1) board.notes.splice(index, 1);
+ })
+ ...
+)
+```
+Similar to `createSubject`, the first argument can be a signal that resets the value of the store. When this signal updates, the store is updated using `reconcile`.
+
+## `createTopic`
+
+A topic combines multiple events into one. This is simply a more convenient way to merge events than manually iterating through them.
+
+```ts
+const [onIncrement, emitIncrement] = createEvent()
+const [onDecrement, emitDecrement] = createEvent()
+
+const onMessage = createTopic(
+ onIncrement(() => `Increment by ${delta}`),
+ onDecrement(() => `Decrement by ${delta}`)
+);
+onMessage(message => console.log(`Message emitted:`, message))
+
+...
+
+emitIncrement(2)
+// logs "Message emitted: Increment by 2"
+
+emitDecrement(1)
+// logs "Message emitted: Decrement by 1"
+```
+
+## `createPartition`
+
+A partition splits an event based on a conditional. This is simply a more convenient way to conditionally split events than using `halt()`.
+
+```ts
+const [onIncrement, emitIncrement] = createEvent()
+
+const [onValidIncrement, onInvalidIncrement] = createPartition(
+ onIncrement,
+ delta => delta > 0
+)
+
+onValidIncrement(delta => console.log(`Valid increment by ${delta}`))
+
+onInvalidIncrement(delta => console.log(`Please use a number greater than 0`))
+
+...
+
+emitIncrement(2)
+// logs "Valid increment by 2"
+
+emitIncrement(0)
+// logs "Please use a number greater than 0"
+
+```
+
+## Use Cases
diff --git a/packages/events/dev/index.tsx b/packages/events/dev/index.tsx
new file mode 100644
index 000000000..22689c0a5
--- /dev/null
+++ b/packages/events/dev/index.tsx
@@ -0,0 +1,105 @@
+import { createEffect, onCleanup } from "solid-js";
+import { createEvent, createSubject, halt } from "../src/index.js";
+
+function Counter() {
+ const [onStart, emitStart] = createEvent();
+ const [onPause, emitPause] = createEvent();
+ const [onSet, emitSet] = createEvent();
+ const [onReset, emitReset] = createEvent();
+ const [onIncrement, emitIncrement] = createEvent();
+ const [onDecrement, emitDecrement] = createEvent();
+ const [onTimerChange, emitTimerChange] = createEvent();
+
+ const onInvalidTimerChange = onTimerChange(n => (n > 0 ? null : true));
+
+ const timerActive = createSubject(
+ true,
+ onStart(() => true),
+ onPause(() => false),
+ onInvalidTimerChange(() => false),
+ );
+
+ const count = createSubject(
+ 0,
+ onSet,
+ onReset(() => 0),
+ onIncrement(() => c => c + 1),
+ onDecrement(() => c => c - 1),
+ );
+
+ const delay = createSubject(
+ 500,
+ onTimerChange(n => (n > 0 ? n : halt())),
+ );
+
+ createEffect(() => {
+ if (delay() && timerActive()) {
+ const i = setInterval(emitIncrement, delay());
+ onCleanup(() => clearInterval(i));
+ }
+ });
+
+ return (
+